Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c856bc79c | ||
|
|
4ebde043a3 | ||
|
|
23280bfd43 | ||
|
|
0db0feba84 | ||
|
|
cb39e5482d | ||
|
|
0e9dafa482 | ||
|
|
c7d4b79180 | ||
|
|
f13bc397d4 | ||
|
|
f84f72e7b6 | ||
|
|
a515c4df73 | ||
|
|
32bf2d0992 |
@@ -15,8 +15,8 @@ android {
|
||||
applicationId = "today.notfeelingit.lcdpet"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 2
|
||||
versionName = "1.1"
|
||||
versionCode = 3
|
||||
versionName = "1.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -47,6 +47,7 @@ dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation("androidx.datastore:datastore-preferences:1.2.1")
|
||||
implementation("androidx.work:work-runtime-ktx:2.11.2")
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@ 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://codeberg.org/KrisKS/LCDPet
|
||||
https://notfeelingit.today/Kris/LCDPet
|
||||
|
||||
The complete license text is included in the source repository's LICENSE file.
|
||||
|
||||
@@ -39,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
|
||||
|
||||
@@ -1,223 +1,816 @@
|
||||
LCDPET HOW TO
|
||||
|
||||
THE GOAL
|
||||
STARTING A NEW PET
|
||||
|
||||
LCDPet is a virtual pet that changes over time while you care for it.
|
||||
When LCDPet starts without an active pet, the CHOOSE NEW PET screen appears.
|
||||
|
||||
Your goal is to build the strongest bond possible while keeping your pet healthy, happy, rested, clean, and cared for.
|
||||
Choose one of these pet templates:
|
||||
|
||||
Pay attention to your pet's needs, respond at the right time, and avoid relying on the same action again and again.
|
||||
Classic
|
||||
Scruffy
|
||||
Round Bean
|
||||
Pocket Pup
|
||||
Batty
|
||||
Goblin
|
||||
|
||||
Regular care matters. Your relationship with the pet grows through the choices you make over time.
|
||||
Tap a preview to select it.
|
||||
|
||||
CARING FOR YOUR PET
|
||||
Enter a name under NAME PET.
|
||||
|
||||
Watch the status bars and messages to see how your pet is doing.
|
||||
Names:
|
||||
|
||||
When your pet needs something, choose the action that fits the situation.
|
||||
Must contain letters only
|
||||
Can be up to 12 letters long
|
||||
Cannot use the reserved names Patch or Petch
|
||||
|
||||
Good care improves the Care record and helps Bond grow.
|
||||
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.
|
||||
|
||||
Missed needs, indoor accidents, overfeeding, sickness, and other harmful situations can increase Neglect and leave your pet in worse condition.
|
||||
|
||||
INSIDE AND OUTSIDE
|
||||
|
||||
Tap the Inside / Outside line near the top of the screen to change locations.
|
||||
Tap the Inside / Outside and weather line near the top of the screen to change location.
|
||||
|
||||
Inside is where most care and social actions happen.
|
||||
Most care and social actions happen Inside.
|
||||
|
||||
Outside is where your pet can walk and safely use the bathroom.
|
||||
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.
|
||||
|
||||
Some situations are safe outside but can cause trouble indoors, so location matters.
|
||||
|
||||
CARE
|
||||
|
||||
Open CARE while Inside to find BATHE, FEED, and NAP.
|
||||
|
||||
|
||||
BATHE
|
||||
|
||||
Bathing reduces Dirtiness and removes stains left by indoor accidents.
|
||||
BATHE is available when Dirtiness is above zero or the pet currently Wants a Bath.
|
||||
|
||||
A pet with a stronger Bond accepts bathing more easily and may become fully clean.
|
||||
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.
|
||||
|
||||
Bathing also helps with recovery when extreme Dirtiness caused sickness.
|
||||
|
||||
FEED
|
||||
|
||||
Feeding lowers the Hunger value.
|
||||
A high Hunger value means the pet needs food.
|
||||
|
||||
A high Hunger value means your pet needs food.
|
||||
A low Hunger value means the pet is already full.
|
||||
|
||||
A very low Hunger value means your 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.
|
||||
|
||||
Feeding an already full pet too many times can cause sickness from overfeeding, so pay attention to Hunger before feeding.
|
||||
|
||||
NAP
|
||||
|
||||
Napping restores Energy and helps your pet recover.
|
||||
NAP is available while Inside whenever Energy is below 10.
|
||||
|
||||
If Energy reaches zero, your pet may eventually fall asleep on its own.
|
||||
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.
|
||||
|
||||
Choosing an indoor nap is safer and calmer than letting your pet collapse from exhaustion.
|
||||
|
||||
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
|
||||
|
||||
Petting improves Happiness and can help Bond grow.
|
||||
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
|
||||
|
||||
Playing improves Happiness.
|
||||
PLAY requires at least 1 Energy.
|
||||
|
||||
It can also use Energy and increase Hunger, so check the status bars afterward.
|
||||
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.
|
||||
|
||||
Using Play repeatedly can eventually increase Boredom instead of helping.
|
||||
|
||||
TALK
|
||||
|
||||
Press and hold TALK while speaking or making sounds.
|
||||
|
||||
The microphone reacts only to how loud the sound is.
|
||||
Release TALK to finish the action.
|
||||
|
||||
LCDPet does not recognize words, create transcripts, save recordings, or upload audio.
|
||||
The microphone graphic changes according to the live sound level.
|
||||
|
||||
Release TALK to finish.
|
||||
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.
|
||||
|
||||
OUTSIDE
|
||||
|
||||
WALK
|
||||
|
||||
Walking restores Health.
|
||||
WALK requires at least 1 Energy.
|
||||
|
||||
It also uses Energy and can increase Hunger and Dirtiness.
|
||||
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.
|
||||
|
||||
A pet that is exhausted, Weak, or Sick may not be able to walk.
|
||||
|
||||
POTTY
|
||||
|
||||
The Potty value increases over time and after meals.
|
||||
POTTY is available Outside when the Potty value is above zero.
|
||||
|
||||
Use Potty while outside to safely reset it.
|
||||
Using POTTY:
|
||||
|
||||
If the value reaches its limit indoors, your pet can have an accident.
|
||||
Resets Potty to 0.
|
||||
Increases Hunger by 1.
|
||||
Adds 1 Care.
|
||||
Records the Potty Need as completed.
|
||||
|
||||
An indoor accident leaves a stain, increases Dirtiness, and adds Neglect.
|
||||
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.
|
||||
|
||||
Take your pet outside before the Potty value reaches its limit.
|
||||
|
||||
WANTS
|
||||
|
||||
Your pet may occasionally ask for something specific.
|
||||
A Want is a temporary request from the pet.
|
||||
|
||||
A Want can ask for Play, Pet, Talk, a Snack, a Bath, or a trip Outside.
|
||||
A new Want appears after a random 8, 12, 18, or 24 hours.
|
||||
|
||||
Completing the request before it disappears gives extra Bond.
|
||||
Each Want lasts 10 minutes.
|
||||
|
||||
An ignored Want eventually expires, but it does not immediately harm your pet.
|
||||
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.
|
||||
|
||||
A Snack is a small treat and does not affect Hunger like a full meal.
|
||||
|
||||
BOND
|
||||
|
||||
Bond shows the strength of your relationship with your pet.
|
||||
Bond measures the relationship between the player and the pet.
|
||||
|
||||
It grows through steady care, meeting Wants, and responding to needs.
|
||||
Bond ranges from 0 to 100.
|
||||
|
||||
Some Wants give more Bond than ordinary actions.
|
||||
The relationship names are:
|
||||
|
||||
Using one action too many times can slow or reverse your progress.
|
||||
0 through 24: New Pet
|
||||
25 through 49: Settled
|
||||
50 through 79: Pal
|
||||
80 through 99: Best Friend
|
||||
100: BFF
|
||||
|
||||
The strongest Bond comes from understanding your pet and giving it balanced care.
|
||||
Ordinary Bond growth can happen once every 6 hours.
|
||||
|
||||
STATUS BARS
|
||||
Some completed Wants bypass that cooldown.
|
||||
|
||||
HAPPINESS
|
||||
The normal Bond gain increases as Bond rises:
|
||||
|
||||
Shows your pet's current mood.
|
||||
Bond 0 through 20: 2 points
|
||||
Bond 21 through 50: 3 points
|
||||
Bond 51 through 80: 5 points
|
||||
Bond 81 through 99: 7 points
|
||||
|
||||
ENERGY
|
||||
Some Wants multiply that normal gain.
|
||||
|
||||
Used by active actions such as Play and Walk.
|
||||
Bond cannot rise above 100.
|
||||
|
||||
Low Energy means your pet needs rest.
|
||||
Repeating one social action 25 times in a row removes 5 Bond once during that streak.
|
||||
|
||||
HUNGER
|
||||
Changing actions resets that streak.
|
||||
|
||||
A high value means your pet needs food.
|
||||
|
||||
A low value means your pet is full.
|
||||
|
||||
DIRTINESS
|
||||
|
||||
Can increase through walking, outdoor time, rain, and accidents.
|
||||
|
||||
High Dirtiness creates a need for a bath and can eventually cause sickness.
|
||||
|
||||
POTTY
|
||||
|
||||
Increases over time and after meals.
|
||||
|
||||
Use Potty outdoors before an indoor accident happens.
|
||||
|
||||
BOREDOM
|
||||
|
||||
Can increase when the same action is used too many times.
|
||||
Boredom can increase when Play, Pet, or Talk is repeated too many times.
|
||||
|
||||
Change activities and pay attention to what your pet needs.
|
||||
The first Boredom increase can happen after a random 3 through 13 repeats.
|
||||
|
||||
HEALTH, SICKNESS, AND RECOVERY
|
||||
Further increases happen at additional random intervals if the same action continues.
|
||||
|
||||
Ignoring serious needs can make your pet Sick or Weak.
|
||||
As Boredom rises, repeated actions remove Happiness and Energy more often.
|
||||
|
||||
Possible causes include starvation, overfeeding, and extreme Dirtiness.
|
||||
At Boredom 9 or 10, each triggered penalty removes 2 Happiness and 2 Energy.
|
||||
|
||||
Messages near the pet tell you what is wrong.
|
||||
Changing to another social action lowers Boredom by 1 and reduces the accumulated penalty.
|
||||
|
||||
Correct the cause before expecting the pet to recover.
|
||||
|
||||
Feed a starving pet.
|
||||
HEALTH AND STATUS
|
||||
|
||||
Do not continue feeding an overfed pet.
|
||||
Health is shown by ten hearts above the pet.
|
||||
|
||||
Bathe a pet that became sick from Dirtiness.
|
||||
Health can be lost through starvation, overfeeding, maximum Dirtiness, or allowing a full Potty meter to overflow Outside.
|
||||
|
||||
Keep a Weak or recovering pet indoors.
|
||||
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.
|
||||
|
||||
Recovery takes time and may also require rest, food, and safe indoor conditions.
|
||||
|
||||
PET RECORD
|
||||
|
||||
The Pet Record shows:
|
||||
|
||||
Bond
|
||||
The strength of your relationship.
|
||||
The current Bond percentage, Bond bar, and relationship name.
|
||||
|
||||
Age
|
||||
How long you have cared for your pet.
|
||||
The pet's current age record.
|
||||
|
||||
Care
|
||||
The positive care you have provided.
|
||||
The number of successful care actions recorded.
|
||||
|
||||
Neglect
|
||||
Missed needs and harmful situations.
|
||||
The number of serious harmful care failures recorded.
|
||||
|
||||
|
||||
Age means time together. It is not a countdown to death.
|
||||
|
||||
CARE JOURNAL
|
||||
|
||||
The Care Journal records important events in your pet's life.
|
||||
Open Stats, then tap CARE JOURNAL.
|
||||
|
||||
It can show completed needs, missed needs, Wants, and other major care moments.
|
||||
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.
|
||||
|
||||
Read it to see how your pet has been treated over time.
|
||||
|
||||
CREDITS AND LICENSES
|
||||
|
||||
Tap Credits / Licenses near the top of the screen to view acknowledgements, sound credits, and license information.
|
||||
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
|
||||
|
||||
CLOSE returns to your pet.
|
||||
How To and Credits / Licenses open as full-screen scrolling panels.
|
||||
|
||||
MOVE places the MOVE and CLOSE buttons in another corner.
|
||||
CLOSE returns to the pet.
|
||||
|
||||
When you move the buttons in How To, they also move in Credits / Licenses, and vice versa.
|
||||
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,18 +45,29 @@ 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
|
||||
fun TouchGrassActivityScreen(
|
||||
lcdDark: Color,
|
||||
onGrassTap: () -> Unit
|
||||
onGrassTap: () -> Unit,
|
||||
statusText: String = "Pet is taking time alone."
|
||||
) {
|
||||
var grassFingerColumn by remember { mutableIntStateOf(-1) }
|
||||
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 +93,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)
|
||||
@@ -108,7 +128,7 @@ fun TouchGrassActivityScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Pet is taking time alone.",
|
||||
text = statusText,
|
||||
color = lcdDark,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
@@ -120,7 +140,8 @@ fun WatchTvActivityScreen(
|
||||
lcdDark: Color,
|
||||
selectedChannel: TvChannel,
|
||||
weatherLabel: String,
|
||||
onChannelSelected: (TvChannel) -> Unit
|
||||
onChannelSelected: (TvChannel) -> Unit,
|
||||
statusText: String = "Pet is outside having alone time."
|
||||
) {
|
||||
var tvFrame by remember { mutableIntStateOf(0) }
|
||||
var tvPowerState by remember { mutableIntStateOf(1) }
|
||||
@@ -128,19 +149,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 +219,12 @@ fun WatchTvActivityScreen(
|
||||
start()
|
||||
}
|
||||
|
||||
while (powerOffPlayer?.isPlaying == true) {
|
||||
while (
|
||||
powerOffPlayer?.isPlaying == true &&
|
||||
lifecycleOwner.lifecycle.currentState.isAtLeast(
|
||||
Lifecycle.State.RESUMED
|
||||
)
|
||||
) {
|
||||
delay(50L)
|
||||
}
|
||||
} finally {
|
||||
@@ -167,34 +233,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,30 +390,78 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Pet is outside having alone time.",
|
||||
text = statusText,
|
||||
color = lcdDark,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
@@ -418,12 +589,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())
|
||||
}
|
||||
@@ -15,6 +15,7 @@ val Context.lcdPetDataStore by preferencesDataStore(name = "lcdpet_save")
|
||||
data class LCDPetRecordSave(
|
||||
val bond: Int = 0,
|
||||
val petAgeDays: Int = 0,
|
||||
val nextAgeAtMs: Long = 0L,
|
||||
val careScore: Int = 0,
|
||||
val neglectScore: Int = 0,
|
||||
val hunger: Int = 5,
|
||||
@@ -24,6 +25,15 @@ data class LCDPetRecordSave(
|
||||
val dirtiness: Int = 0,
|
||||
val potty: Int = 0,
|
||||
val boredom: Int = 0,
|
||||
val starvingCount: Int = 0,
|
||||
val recoveryCount: Int = 0,
|
||||
val sickCount: Int = 0,
|
||||
val playEnergyCount: Int = 0,
|
||||
val sameActionStreak: Int = 0,
|
||||
val boredomTriggerAt: Int = 0,
|
||||
val boredomPenaltyCount: Int = 0,
|
||||
val ultraTrollTriggered: Boolean = false,
|
||||
val lastActionName: String = "",
|
||||
val location: String = "Inside",
|
||||
val weather: String = "Sunny",
|
||||
val status: String = "Nervous",
|
||||
@@ -35,19 +45,34 @@ data class LCDPetRecordSave(
|
||||
val tradeInProgress: Int = 0,
|
||||
val tradeInStartedAtMs: Long = 0L,
|
||||
val currentWant: String = "",
|
||||
val currentWantStartedAtMs: Long = 0L,
|
||||
val nextWantAtMs: Long = 0L,
|
||||
val petName: String = "",
|
||||
val petTemplateId: String = "classic",
|
||||
val isSleeping: Boolean = false,
|
||||
val sleepMode: String = "",
|
||||
val roughWakeAlert: Boolean = false,
|
||||
val autoSleepStartedAtMs: Long = 0L,
|
||||
val outsideEnergyNextTickAtMs: Long = 0L,
|
||||
val nextWeatherAtMs: Long = 0L,
|
||||
val bondReadyAtMs: Long = 0L,
|
||||
val careCycleNextTickAtMs: Long = 0L,
|
||||
val rainHappinessNextTickAtMs: Long = 0L,
|
||||
val pottyNextTickAtMs: Long = 0L,
|
||||
val dirtyHealthNextTickAtMs: Long = 0L,
|
||||
val careJournalRaw: String = "",
|
||||
val lastSavedAtMs: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
data class LCDPetCreatedWant(
|
||||
val wantName: String,
|
||||
val petName: String
|
||||
)
|
||||
|
||||
private object LCDPetSaveKeys {
|
||||
val BOND = intPreferencesKey("bond")
|
||||
val PET_AGE_DAYS = intPreferencesKey("pet_age_days")
|
||||
val NEXT_AGE_AT_MS = longPreferencesKey("next_age_at_ms")
|
||||
val CARE_SCORE = intPreferencesKey("care_score")
|
||||
val NEGLECT_SCORE = intPreferencesKey("neglect_score")
|
||||
val HUNGER = intPreferencesKey("hunger")
|
||||
@@ -57,6 +82,17 @@ private object LCDPetSaveKeys {
|
||||
val DIRTINESS = intPreferencesKey("dirtiness")
|
||||
val POTTY = intPreferencesKey("potty")
|
||||
val BOREDOM = intPreferencesKey("boredom")
|
||||
val STARVING_COUNT = intPreferencesKey("starving_count")
|
||||
val RECOVERY_COUNT = intPreferencesKey("recovery_count")
|
||||
val SICK_COUNT = intPreferencesKey("sick_count")
|
||||
val PLAY_ENERGY_COUNT = intPreferencesKey("play_energy_count")
|
||||
val SAME_ACTION_STREAK = intPreferencesKey("same_action_streak")
|
||||
val BOREDOM_TRIGGER_AT = intPreferencesKey("boredom_trigger_at")
|
||||
val BOREDOM_PENALTY_COUNT =
|
||||
intPreferencesKey("boredom_penalty_count")
|
||||
val ULTRA_TROLL_TRIGGERED =
|
||||
booleanPreferencesKey("ultra_troll_triggered")
|
||||
val LAST_ACTION_NAME = stringPreferencesKey("last_action_name")
|
||||
val LOCATION = stringPreferencesKey("location")
|
||||
val WEATHER = stringPreferencesKey("weather")
|
||||
val STATUS = stringPreferencesKey("status")
|
||||
@@ -68,13 +104,32 @@ private object LCDPetSaveKeys {
|
||||
val TRADE_IN_PROGRESS = intPreferencesKey("trade_in_progress")
|
||||
val TRADE_IN_STARTED_AT_MS = longPreferencesKey("trade_in_started_at_ms")
|
||||
val CURRENT_WANT = stringPreferencesKey("current_want")
|
||||
val CURRENT_WANT_STARTED_AT_MS =
|
||||
longPreferencesKey("current_want_started_at_ms")
|
||||
val NEXT_WANT_AT_MS = longPreferencesKey("next_want_at_ms")
|
||||
val PET_NAME = stringPreferencesKey("pet_name")
|
||||
val PET_TEMPLATE_ID = stringPreferencesKey("pet_template_id")
|
||||
val IS_SLEEPING = booleanPreferencesKey("is_sleeping")
|
||||
val SLEEP_MODE = stringPreferencesKey("sleep_mode")
|
||||
val ROUGH_WAKE_ALERT = booleanPreferencesKey("rough_wake_alert")
|
||||
val AUTO_SLEEP_STARTED_AT_MS = longPreferencesKey("auto_sleep_started_at_ms")
|
||||
val OUTSIDE_ENERGY_NEXT_TICK_AT_MS =
|
||||
longPreferencesKey("outside_energy_next_tick_at_ms")
|
||||
val NEXT_WEATHER_AT_MS =
|
||||
longPreferencesKey("next_weather_at_ms")
|
||||
val BOND_READY_AT_MS =
|
||||
longPreferencesKey("bond_ready_at_ms")
|
||||
val CARE_CYCLE_NEXT_TICK_AT_MS =
|
||||
longPreferencesKey("care_cycle_next_tick_at_ms")
|
||||
val RAIN_HAPPINESS_NEXT_TICK_AT_MS =
|
||||
longPreferencesKey("rain_happiness_next_tick_at_ms")
|
||||
val POTTY_NEXT_TICK_AT_MS =
|
||||
longPreferencesKey("potty_next_tick_at_ms")
|
||||
val DIRTY_HEALTH_NEXT_TICK_AT_MS =
|
||||
longPreferencesKey("dirty_health_next_tick_at_ms")
|
||||
val CARE_JOURNAL = stringPreferencesKey("care_journal")
|
||||
val APPEARANCE = stringPreferencesKey("appearance")
|
||||
val PET_HOME_COLOR = stringPreferencesKey("pet_home_color")
|
||||
val LAST_SAVED_AT_MS = longPreferencesKey("last_saved_at_ms")
|
||||
}
|
||||
|
||||
@@ -83,6 +138,8 @@ val Context.lcdPetRecordSaveFlow: Flow<LCDPetRecordSave>
|
||||
LCDPetRecordSave(
|
||||
bond = preferences[LCDPetSaveKeys.BOND] ?: 0,
|
||||
petAgeDays = preferences[LCDPetSaveKeys.PET_AGE_DAYS] ?: 0,
|
||||
nextAgeAtMs =
|
||||
preferences[LCDPetSaveKeys.NEXT_AGE_AT_MS] ?: 0L,
|
||||
careScore = preferences[LCDPetSaveKeys.CARE_SCORE] ?: 0,
|
||||
neglectScore = preferences[LCDPetSaveKeys.NEGLECT_SCORE] ?: 0,
|
||||
hunger = preferences[LCDPetSaveKeys.HUNGER] ?: 5,
|
||||
@@ -92,6 +149,24 @@ val Context.lcdPetRecordSaveFlow: Flow<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",
|
||||
@@ -103,17 +178,73 @@ val Context.lcdPetRecordSaveFlow: Flow<LCDPetRecordSave>
|
||||
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,
|
||||
@@ -126,6 +257,15 @@ suspend fun Context.saveLCDPetStats(
|
||||
dirtiness: Int,
|
||||
potty: Int,
|
||||
boredom: Int,
|
||||
starvingCount: Int,
|
||||
recoveryCount: Int,
|
||||
sickCount: Int,
|
||||
playEnergyCount: Int,
|
||||
sameActionStreak: Int,
|
||||
boredomTriggerAt: Int,
|
||||
boredomPenaltyCount: Int,
|
||||
ultraTrollTriggered: Boolean,
|
||||
lastActionName: String,
|
||||
location: String,
|
||||
weather: String,
|
||||
status: String,
|
||||
@@ -143,6 +283,14 @@ suspend fun Context.saveLCDPetStats(
|
||||
sleepMode: String,
|
||||
roughWakeAlert: Boolean,
|
||||
autoSleepStartedAtMs: Long,
|
||||
currentWantStartedAtMs: Long = 0L,
|
||||
nextWantAtMs: Long = 0L,
|
||||
nextWeatherAtMs: Long,
|
||||
bondReadyAtMs: Long,
|
||||
careCycleNextTickAtMs: Long,
|
||||
rainHappinessNextTickAtMs: Long,
|
||||
pottyNextTickAtMs: Long,
|
||||
dirtyHealthNextTickAtMs: Long,
|
||||
careJournalRaw: String? = null
|
||||
) {
|
||||
lcdPetDataStore.edit { preferences ->
|
||||
@@ -157,6 +305,24 @@ suspend fun Context.saveLCDPetStats(
|
||||
preferences[LCDPetSaveKeys.DIRTINESS] = dirtiness
|
||||
preferences[LCDPetSaveKeys.POTTY] = potty
|
||||
preferences[LCDPetSaveKeys.BOREDOM] = boredom
|
||||
preferences[LCDPetSaveKeys.STARVING_COUNT] =
|
||||
starvingCount.coerceAtLeast(0)
|
||||
preferences[LCDPetSaveKeys.RECOVERY_COUNT] =
|
||||
recoveryCount.coerceAtLeast(0)
|
||||
preferences[LCDPetSaveKeys.SICK_COUNT] =
|
||||
sickCount.coerceAtLeast(0)
|
||||
preferences[LCDPetSaveKeys.PLAY_ENERGY_COUNT] =
|
||||
playEnergyCount.coerceAtLeast(0)
|
||||
preferences[LCDPetSaveKeys.SAME_ACTION_STREAK] =
|
||||
sameActionStreak.coerceAtLeast(0)
|
||||
preferences[LCDPetSaveKeys.BOREDOM_TRIGGER_AT] =
|
||||
boredomTriggerAt.coerceAtLeast(0)
|
||||
preferences[LCDPetSaveKeys.BOREDOM_PENALTY_COUNT] =
|
||||
boredomPenaltyCount.coerceAtLeast(0)
|
||||
preferences[LCDPetSaveKeys.ULTRA_TROLL_TRIGGERED] =
|
||||
ultraTrollTriggered
|
||||
preferences[LCDPetSaveKeys.LAST_ACTION_NAME] =
|
||||
lastActionName
|
||||
preferences[LCDPetSaveKeys.LOCATION] = location
|
||||
preferences[LCDPetSaveKeys.WEATHER] = weather
|
||||
preferences[LCDPetSaveKeys.STATUS] = status
|
||||
@@ -168,6 +334,22 @@ suspend fun Context.saveLCDPetStats(
|
||||
preferences[LCDPetSaveKeys.TRADE_IN_PROGRESS] = tradeInProgress
|
||||
preferences[LCDPetSaveKeys.TRADE_IN_STARTED_AT_MS] = tradeInStartedAtMs
|
||||
preferences[LCDPetSaveKeys.CURRENT_WANT] = currentWant
|
||||
preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] =
|
||||
currentWantStartedAtMs.coerceAtLeast(0L)
|
||||
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] =
|
||||
nextWantAtMs.coerceAtLeast(0L)
|
||||
preferences[LCDPetSaveKeys.NEXT_WEATHER_AT_MS] =
|
||||
nextWeatherAtMs.coerceAtLeast(0L)
|
||||
preferences[LCDPetSaveKeys.BOND_READY_AT_MS] =
|
||||
bondReadyAtMs.coerceAtLeast(0L)
|
||||
preferences[LCDPetSaveKeys.CARE_CYCLE_NEXT_TICK_AT_MS] =
|
||||
careCycleNextTickAtMs.coerceAtLeast(0L)
|
||||
preferences[LCDPetSaveKeys.RAIN_HAPPINESS_NEXT_TICK_AT_MS] =
|
||||
rainHappinessNextTickAtMs.coerceAtLeast(0L)
|
||||
preferences[LCDPetSaveKeys.POTTY_NEXT_TICK_AT_MS] =
|
||||
pottyNextTickAtMs.coerceAtLeast(0L)
|
||||
preferences[LCDPetSaveKeys.DIRTY_HEALTH_NEXT_TICK_AT_MS] =
|
||||
dirtyHealthNextTickAtMs.coerceAtLeast(0L)
|
||||
preferences[LCDPetSaveKeys.PET_NAME] = petName
|
||||
preferences[LCDPetSaveKeys.PET_TEMPLATE_ID] = petTemplateId
|
||||
preferences[LCDPetSaveKeys.IS_SLEEPING] = isSleeping
|
||||
@@ -180,3 +362,88 @@ suspend fun Context.saveLCDPetStats(
|
||||
preferences[LCDPetSaveKeys.LAST_SAVED_AT_MS] = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Context.createLCDPetWantIfDue(
|
||||
nowMs: Long = System.currentTimeMillis()
|
||||
): LCDPetCreatedWant? {
|
||||
var createdWant: LCDPetCreatedWant? = null
|
||||
|
||||
lcdPetDataStore.edit { preferences ->
|
||||
val currentWant =
|
||||
preferences[LCDPetSaveKeys.CURRENT_WANT].orEmpty()
|
||||
val nextWantAtMs =
|
||||
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] ?: 0L
|
||||
val petName =
|
||||
preferences[LCDPetSaveKeys.PET_NAME].orEmpty()
|
||||
val location =
|
||||
preferences[LCDPetSaveKeys.LOCATION] ?: "Inside"
|
||||
val bond =
|
||||
preferences[LCDPetSaveKeys.BOND] ?: 0
|
||||
val status =
|
||||
preferences[LCDPetSaveKeys.STATUS] ?: "Nervous"
|
||||
val isSleeping =
|
||||
preferences[LCDPetSaveKeys.IS_SLEEPING] ?: false
|
||||
val isTradingInPet =
|
||||
preferences[LCDPetSaveKeys.IS_TRADING_IN_PET] ?: false
|
||||
val isFindingNewPet =
|
||||
preferences[LCDPetSaveKeys.IS_FINDING_NEW_PET] ?: false
|
||||
|
||||
val canCreateWant =
|
||||
currentWant.isBlank() &&
|
||||
nextWantAtMs > 0L &&
|
||||
nowMs >= nextWantAtMs &&
|
||||
petName.isNotBlank() &&
|
||||
status !in setOf("Sick", "Weak") &&
|
||||
!isSleeping &&
|
||||
!isTradingInPet &&
|
||||
!isFindingNewPet
|
||||
|
||||
if (!canCreateWant) {
|
||||
return@edit
|
||||
}
|
||||
|
||||
val possibleWants =
|
||||
if (location == "Inside") {
|
||||
listOf("Snack", "Play", "Pet", "Talk", "Outside") +
|
||||
if (bond >= 100) listOf("Bath") else emptyList()
|
||||
} else {
|
||||
listOf("Play", "Pet", "Talk")
|
||||
}
|
||||
|
||||
val wantName = possibleWants.random()
|
||||
|
||||
preferences[LCDPetSaveKeys.CURRENT_WANT] = wantName
|
||||
preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] = nowMs
|
||||
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] = 0L
|
||||
preferences[LCDPetSaveKeys.LAST_SAVED_AT_MS] = nowMs
|
||||
|
||||
createdWant = LCDPetCreatedWant(
|
||||
wantName = wantName,
|
||||
petName = petName
|
||||
)
|
||||
}
|
||||
|
||||
return createdWant
|
||||
}
|
||||
|
||||
suspend fun Context.clearLCDPetWants() {
|
||||
lcdPetDataStore.edit { preferences ->
|
||||
preferences[LCDPetSaveKeys.CURRENT_WANT] = ""
|
||||
preferences[LCDPetSaveKeys.CURRENT_WANT_STARTED_AT_MS] = 0L
|
||||
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] = 0L
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Context.setLCDPetNextWantAtMs(nextWantAtMs: Long) {
|
||||
lcdPetDataStore.edit { preferences ->
|
||||
preferences[LCDPetSaveKeys.NEXT_WANT_AT_MS] =
|
||||
nextWantAtMs.coerceAtLeast(0L)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Context.setLCDPetOutsideEnergyNextTickAtMs(nextTickAtMs: Long) {
|
||||
lcdPetDataStore.edit { preferences ->
|
||||
preferences[LCDPetSaveKeys.OUTSIDE_ENERGY_NEXT_TICK_AT_MS] =
|
||||
nextTickAtMs.coerceAtLeast(0L)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package today.notfeelingit.lcdpet.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@@ -8,25 +10,24 @@ private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
)
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LCDPetTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = LightColorScheme,
|
||||
colorScheme =
|
||||
if (darkTheme) DarkColorScheme
|
||||
else LightColorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 53 KiB |