From 367e34ef18ac1af173edf7e7dc14f0b926903ba5 Mon Sep 17 00:00:00 2001 From: Sarahtonin Date: Wed, 2 Sep 2026 22:40:58 +0200 Subject: [PATCH] Added plant info upgrade. --- Pages/Garden.php | 1 + Pages/Profile.php | 37 ++++++ Scripts/Garden/Garden.js | 265 +++++++++++++++++++++++++++++++++++-- Scripts/Garden/Profile.js | 151 +++++++++++++++++++++ Scripts/Garden/Save.js | 22 ++- Scripts/Garden/Shop.js | 177 ++++++++++++++++++++++++- Scripts/Garden/Upgrades.js | 35 +++++ Styles/Style.css | 55 ++++++++ 8 files changed, 733 insertions(+), 10 deletions(-) diff --git a/Pages/Garden.php b/Pages/Garden.php index b553e58..ebf58f3 100644 --- a/Pages/Garden.php +++ b/Pages/Garden.php @@ -137,6 +137,7 @@ $PageSection = "Garden"; + diff --git a/Pages/Profile.php b/Pages/Profile.php index c32db7b..ce108e4 100644 --- a/Pages/Profile.php +++ b/Pages/Profile.php @@ -99,6 +99,42 @@ $PageTitle = "Profile"; +
+

Garden display

+ +

+ Choose which Plant information details are + shown on Garden plots. +

+ +
+ + + +
+ +

+ Unlock Plant information in the Shop + to use these settings. +

+
+
+ diff --git a/Scripts/Garden/Garden.js b/Scripts/Garden/Garden.js index b473a06..59832b5 100644 --- a/Scripts/Garden/Garden.js +++ b/Scripts/Garden/Garden.js @@ -327,12 +327,18 @@ function RenderGarden() { GardenGrid.replaceChildren(); + + const DisplaySettings = + GetGardenDisplaySettings(); + + GameSave.Garden.Plots.forEach( (Plot, PlotIndex) => { const PlotElement = CreatePlotElement( Plot, - PlotIndex + PlotIndex, + DisplaySettings ); GardenGrid.appendChild( @@ -343,9 +349,32 @@ function RenderGarden() { } +function GetGardenDisplaySettings() { + const HasPlantInformation = + HasPlantInformationUpgrade( + GameSave + ); + + return { + ShowPlantNames: + HasPlantInformation && + GameSave.Preferences + .ShowPlantNames !== + false, + + ShowGrowthTimers: + HasPlantInformation && + GameSave.Preferences + .ShowGrowthTimers !== + false + }; +} + + function CreatePlotElement( Plot, - PlotIndex + PlotIndex, + DisplaySettings ) { const Button = document.createElement( @@ -358,6 +387,53 @@ function CreatePlotElement( Button.type = "button"; + if ( + DisplaySettings.ShowPlantNames + ) { + Button.classList.add( + "GardenPlotWithName" + ); + } + + if ( + DisplaySettings.ShowGrowthTimers + ) { + Button.classList.add( + "GardenPlotWithTimer" + ); + } + + + const Plant = + Plot === null + ? null + : Plants[Plot.Plant]; + + if ( + DisplaySettings.ShowPlantNames + ) { + Button.appendChild( + CreateGardenPlotName( + Plot, + Plant + ) + ); + } + + + const Visual = + document.createElement( + "span" + ); + + Visual.className = + "GardenPlotVisual"; + + Button.appendChild( + Visual + ); + + if (Plot === null) { Button.classList.add( "GardenPlotEmpty" @@ -366,6 +442,19 @@ function CreatePlotElement( Button.title = "Empty plot"; + if ( + DisplaySettings + .ShowGrowthTimers + ) { + Button.appendChild( + CreateGardenPlotTimer( + null, + null, + 0 + ) + ); + } + if ( SelectedGardenTool === "Seed" @@ -384,11 +473,21 @@ function CreatePlotElement( } - const Plant = - Plants[Plot.Plant]; - if (Plant === undefined) { - Button.textContent = "?"; + Visual.textContent = "?"; + + if ( + DisplaySettings + .ShowGrowthTimers + ) { + Button.appendChild( + CreateGardenPlotTimer( + Plot, + null, + 0 + ) + ); + } return Button; } @@ -427,7 +526,7 @@ function CreatePlotElement( Image.src = ImagePath; - Button.appendChild( + Visual.appendChild( Image ); } else { @@ -442,12 +541,25 @@ function CreatePlotElement( MissingImage.textContent = Plant.Name; - Button.appendChild( + Visual.appendChild( MissingImage ); } + if ( + DisplaySettings.ShowGrowthTimers + ) { + Button.appendChild( + CreateGardenPlotTimer( + Plot, + Plant, + Progress + ) + ); + } + + if ( SelectedGardenTool === "Shovel" @@ -507,6 +619,143 @@ function CreatePlotElement( } +function CreateGardenPlotName( + Plot, + Plant +) { + const Name = + document.createElement( + "span" + ); + + Name.className = + "GardenPlotName"; + + if (Plot === null) { + Name.textContent = + "Empty"; + } else if (Plant === undefined) { + Name.textContent = + "Unknown"; + } else { + Name.textContent = + Plant.Name; + + Name.title = + Plant.Name; + } + + return Name; +} + + +function CreateGardenPlotTimer( + Plot, + Plant, + Progress +) { + const Timer = + document.createElement( + "span" + ); + + Timer.className = + "GardenPlotTimer"; + + if ( + Plot === null || + Plant === null || + Plant === undefined + ) { + Timer.textContent = ""; + + return Timer; + } + + + if (Progress >= 1) { + Timer.textContent = + "Ready"; + + return Timer; + } + + + const RemainingTime = + Math.max( + 0, + Plant.GrowthTime - + ( + Date.now() - + Plot.PlantedAt + ) + ); + + Timer.textContent = + FormatGardenRemainingTime( + RemainingTime + ); + + return Timer; +} + + +function FormatGardenRemainingTime( + Milliseconds +) { + const TotalSeconds = + Math.max( + 0, + Math.ceil( + Number( + Milliseconds + ) / 1000 + ) + ); + + const Hours = + Math.floor( + TotalSeconds / 3600 + ); + + const Minutes = + Math.floor( + ( + TotalSeconds % 3600 + ) / 60 + ); + + const Seconds = + TotalSeconds % 60; + + + if (Hours > 0) { + return ( + Hours + + ":" + + String(Minutes).padStart( + 2, + "0" + ) + + ":" + + String(Seconds).padStart( + 2, + "0" + ) + ); + } + + return ( + Minutes + + ":" + + String(Seconds).padStart( + 2, + "0" + ) + ); +} + + async function PlantSeed( PlotIndex ) { diff --git a/Scripts/Garden/Profile.js b/Scripts/Garden/Profile.js index e1884b7..9e33de4 100644 --- a/Scripts/Garden/Profile.js +++ b/Scripts/Garden/Profile.js @@ -11,6 +11,8 @@ async function StartProfile() { await LoadGame(); RenderProfileStatistics(); + RenderGardenDisplayPreferences(); + BindGardenDisplayPreferences(); } catch (Error) { console.error( "Couldn't load profile statistics:", @@ -72,6 +74,155 @@ async function StartProfile() { } +function BindGardenDisplayPreferences() { + const PlantNamesButton = + document.getElementById( + "TogglePlantNamesButton" + ); + + const GrowthTimersButton = + document.getElementById( + "ToggleGrowthTimersButton" + ); + + if ( + PlantNamesButton === null || + GrowthTimersButton === null + ) { + return; + } + + + PlantNamesButton.addEventListener( + "click", + () => { + ToggleGardenDisplayPreference( + "ShowPlantNames" + ); + } + ); + + GrowthTimersButton.addEventListener( + "click", + () => { + ToggleGardenDisplayPreference( + "ShowGrowthTimers" + ); + } + ); +} + + +function RenderGardenDisplayPreferences() { + const PlantNamesButton = + document.getElementById( + "TogglePlantNamesButton" + ); + + const GrowthTimersButton = + document.getElementById( + "ToggleGrowthTimersButton" + ); + + const Message = + document.getElementById( + "GardenDisplayMessage" + ); + + if ( + PlantNamesButton === null || + GrowthTimersButton === null || + Message === null + ) { + return; + } + + + const IsOwned = + HasPlantInformationUpgrade( + ProfileSave + ); + + if (!IsOwned) { + PlantNamesButton.textContent = + "Plant names: Locked"; + + GrowthTimersButton.textContent = + "Growth timers: Locked"; + + PlantNamesButton.disabled = true; + GrowthTimersButton.disabled = true; + + Message.textContent = + "Unlock Plant information in the Shop to use these settings."; + + return; + } + + + const ShowPlantNames = + ProfileSave.Preferences + .ShowPlantNames !== false; + + const ShowGrowthTimers = + ProfileSave.Preferences + .ShowGrowthTimers !== false; + + PlantNamesButton.disabled = false; + GrowthTimersButton.disabled = false; + + PlantNamesButton.textContent = + "Plant names: " + + (ShowPlantNames + ? "On" + : "Off"); + + GrowthTimersButton.textContent = + "Growth timers: " + + (ShowGrowthTimers + ? "On" + : "Off"); + + Message.textContent = + "Plant names and growth timers can be changed independently."; +} + + +async function ToggleGardenDisplayPreference( + PreferenceName +) { + if ( + !HasPlantInformationUpgrade( + ProfileSave + ) + ) { + return; + } + + if ( + PreferenceName !== + "ShowPlantNames" && + PreferenceName !== + "ShowGrowthTimers" + ) { + return; + } + + + ProfileSave.Preferences[ + PreferenceName + ] = !ProfileSave.Preferences[ + PreferenceName + ]; + + RenderGardenDisplayPreferences(); + + await SaveGame( + ProfileSave + ); +} + + async function RenderProfilePlayer() { const UsernameInput = document.getElementById( diff --git a/Scripts/Garden/Save.js b/Scripts/Garden/Save.js index 68da5d7..4ceeb62 100644 --- a/Scripts/Garden/Save.js +++ b/Scripts/Garden/Save.js @@ -66,7 +66,13 @@ function CreateNewSave() { }, Upgrades: { - PlotSize: 0 + PlotSize: 0, + PlantInformation: false + }, + + Preferences: { + ShowPlantNames: true, + ShowGrowthTimers: true }, Garden: { @@ -239,6 +245,20 @@ function NormalizeSaveData( SaveData.Upgrades ??= {}; SaveData.Upgrades.PlotSize ??= 0; + SaveData.Upgrades.PlantInformation = + SaveData.Upgrades.PlantInformation === + true; + + + SaveData.Preferences ??= {}; + + SaveData.Preferences.ShowPlantNames = + SaveData.Preferences.ShowPlantNames !== + false; + + SaveData.Preferences.ShowGrowthTimers = + SaveData.Preferences.ShowGrowthTimers !== + false; SaveData.Garden ??= { diff --git a/Scripts/Garden/Shop.js b/Scripts/Garden/Shop.js index 0e55f11..35b33ab 100644 --- a/Scripts/Garden/Shop.js +++ b/Scripts/Garden/Shop.js @@ -123,7 +123,8 @@ function RenderGardenUpgrades() { ), CreateGardenExpansionCard( "Row" - ) + ), + CreatePlantInformationUpgradeCard() ); } @@ -270,6 +271,122 @@ function CreateGardenExpansionCard( } +function CreatePlantInformationUpgradeCard() { + const Cost = + GetPlantInformationUpgradeCost(); + + const IsOwned = + HasPlantInformationUpgrade( + ShopSave + ); + + + const Card = + document.createElement( + "article" + ); + + Card.className = + "Panel ShopItem"; + + + const Header = + document.createElement( + "div" + ); + + Header.className = + "ShopItemHeader"; + + + const Name = + document.createElement( + "h3" + ); + + Name.textContent = + "Plant information"; + + Header.appendChild(Name); + + + const Description = + document.createElement( + "p" + ); + + Description.textContent = + "Show each plant's name above its sprite and exact time remaining below it. Both displays can be toggled on or off from your Profile."; + + + const Details = + document.createElement( + "div" + ); + + Details.className = + "ShopItemDetails"; + + Details.append( + CreateShopStat( + "Plant names", + IsOwned + ? "Unlocked" + : "Locked" + ), + CreateShopStat( + "Growth timers", + IsOwned + ? "Unlocked" + : "Locked" + ) + ); + + + const BuyButton = + document.createElement( + "button" + ); + + BuyButton.className = + "ActionButton ShopBuyButton"; + + BuyButton.type = "button"; + + if (IsOwned) { + BuyButton.textContent = + "Purchased"; + + BuyButton.disabled = true; + } else { + BuyButton.textContent = + "Buy - " + + Cost.toLocaleString() + + " Dew"; + + BuyButton.disabled = + ShopSave.Currency.Dew < Cost || + ShopPurchasePending; + + BuyButton.addEventListener( + "click", + BuyPlantInformationUpgrade + ); + } + + + Card.append( + Header, + Description, + Details, + BuyButton + ); + + + return Card; +} + + function CreateShopSeedCard( Plant ) { @@ -839,6 +956,64 @@ async function BuyGardenExpansion( } +async function BuyPlantInformationUpgrade() { + if ( + ShopPurchasePending || + HasPlantInformationUpgrade( + ShopSave + ) + ) { + return; + } + + + const Cost = + GetPlantInformationUpgradeCost(); + + if ( + ShopSave.Currency.Dew < + Cost + ) { + SetShopMessage( + "You don't have enough Dew for Plant information." + ); + + return; + } + + + ShopPurchasePending = true; + + try { + ShopSave.Currency.Dew -= + Cost; + + ShopSave.Statistics.CurrencySpent.Dew += + Cost; + + UnlockPlantInformation( + ShopSave + ); + + + SetShopMessage( + "Bought Plant information for " + + Cost.toLocaleString() + + " Dew. Plant names and growth timers are now visible." + ); + + RenderShop(); + + await SaveGame( + ShopSave + ); + } finally { + ShopPurchasePending = false; + RenderShop(); + } +} + + function SetShopMessage( Message ) { diff --git a/Scripts/Garden/Upgrades.js b/Scripts/Garden/Upgrades.js index 39d311a..ce2daad 100644 --- a/Scripts/Garden/Upgrades.js +++ b/Scripts/Garden/Upgrades.js @@ -1,4 +1,5 @@ const GardenExpansionBaseCost = 1000; +const PlantInformationUpgradeCost = 500; function GetGardenColumnUpgradeCost( @@ -107,3 +108,37 @@ function ResetGardenMutationCooldowns( ) { SaveData.MutationCooldowns = {}; } + + +function HasPlantInformationUpgrade( + SaveData +) { + return ( + SaveData + ?.Upgrades + ?.PlantInformation === + true + ); +} + + +function GetPlantInformationUpgradeCost() { + return PlantInformationUpgradeCost; +} + + +function UnlockPlantInformation( + SaveData +) { + SaveData.Upgrades ??= {}; + SaveData.Preferences ??= {}; + + SaveData.Upgrades.PlantInformation = + true; + + SaveData.Preferences.ShowPlantNames = + true; + + SaveData.Preferences.ShowGrowthTimers = + true; +} diff --git a/Styles/Style.css b/Styles/Style.css index dff1a31..04e7af1 100644 --- a/Styles/Style.css +++ b/Styles/Style.css @@ -328,6 +328,7 @@ input, button, textarea, select { --PlotBorderColour: var(--Surface2); --TileBackground: var(--PlantBackground); position: relative; + display: grid; width: 100%; aspect-ratio: 1; padding: 0; @@ -337,6 +338,52 @@ input, button, textarea, select { box-shadow: 3px 3px 0 var(--Crust); } +.GardenPlotWithName, +.GardenPlotWithTimer { + aspect-ratio: auto; +} + +.GardenPlotVisual { + display: flex; + width: 100%; + min-width: 0; + aspect-ratio: 1; + align-items: center; + justify-content: center; + overflow: hidden; + background-color: var(--TileBackground); +} + +.GardenPlotName, +.GardenPlotTimer { + display: flex; + min-width: 0; + min-height: 1.5rem; + padding: 0.2rem 0.3rem; + align-items: center; + justify-content: center; + overflow: hidden; + color: var(--Subtext1); + background-color: var(--Mantle); + font-size: 0.75rem; + line-height: 1; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.GardenPlotName { + border-bottom: 2px solid var(--Surface2); +} + +.GardenPlotTimer { + border-top: 2px solid var(--Surface2); +} + +.GardenPlotMature .GardenPlotTimer { + color: var(--Green); +} + .GardenPlot:hover { --TileBackground: var(--Surface1); --PlotBorderColour: var(--Sky); @@ -645,6 +692,14 @@ input, button, textarea, select { box-shadow: 2px 2px 0 var(--Crust); } +.ProfileSection button:disabled { + cursor: not-allowed; + color: var(--Overlay1); + background-color: var(--Surface0); + border-color: var(--Surface1); + box-shadow: none; +} + .ProfileSection button:active { color: var(--Green); background-color: var(--Surface2);