From d72045d6151a850b88a4e5179de9644993d1c7be Mon Sep 17 00:00:00 2001 From: Sarahtonin Date: Thu, 3 Sep 2026 18:07:19 +0200 Subject: [PATCH] Added multiple gardens per user upgrade. Pretty cool!!!! --- Pages/Garden.php | 32 +++ Pages/Shop.php | 4 +- Scripts/Garden/Garden.js | 247 ++++++++++++++++++- Scripts/Garden/Save.js | 493 +++++++++++++++++++++++++++++-------- Scripts/Garden/Shop.js | 168 +++++++++++++ Scripts/Garden/Upgrades.js | 48 ++++ Styles/Style.css | 41 ++- 7 files changed, 930 insertions(+), 103 deletions(-) diff --git a/Pages/Garden.php b/Pages/Garden.php index f673a41..be5f995 100644 --- a/Pages/Garden.php +++ b/Pages/Garden.php @@ -78,6 +78,38 @@ $PageSection = "Garden"; +
+ + + + + +
+

Plots

diff --git a/Pages/Shop.php b/Pages/Shop.php index 54ea479..0c9c2ec 100644 --- a/Pages/Shop.php +++ b/Pages/Shop.php @@ -47,8 +47,8 @@ $PageSection = "Shop";

Permanent upgrades

- Permanently expand your Garden or unlock - extra information and quality-of-life features. + Expand your active Garden, buy additional Gardens, + or unlock information and quality-of-life features.

{ + SwitchGarden(-1); + } + ); + + NextButton?.addEventListener( + "click", + () => { + SwitchGarden(1); + } + ); + + NameInput?.addEventListener( + "change", + async () => { + const GardenIndex = + GameSave.ActiveGardenIndex; + + const GardenName = + RenameGarden( + GameSave, + GardenIndex, + NameInput.value + ); + + NameInput.value = + GardenName; + + SetGardenMessage( + "Garden renamed to " + + GardenName + + "." + ); + + await SaveGame( + GameSave + ); + } + ); + + NameInput?.addEventListener( + "keydown", + Event => { + if (Event.key !== "Enter") { + return; + } + + Event.preventDefault(); + NameInput.blur(); + } + ); +} + + +async function SwitchGarden( + Direction +) { + const GardenCount = + GameSave.Gardens.length; + + if (GardenCount <= 1) { + return; + } + + const NewIndex = + GameSave.ActiveGardenIndex + + Direction; + + if ( + NewIndex < 0 || + NewIndex >= GardenCount + ) { + return; + } + + SetActiveGardenIndex( + GameSave, + NewIndex + ); + + const MutationResult = + CheckGardenMutations( + GameSave + ); + + RenderGame(); + + SetGardenMessage( + "Switched to " + + GameSave.Garden.Name + + "." + ); + + await SaveGame( + GameSave + ); + + if (MutationResult.Attempted) { + RenderGame(); + } +} + + +function RenderGardenSelector() { + const PreviousButton = + document.getElementById( + "PreviousGardenButton" + ); + + const NextButton = + document.getElementById( + "NextGardenButton" + ); + + const NameInput = + document.getElementById( + "GardenNameInput" + ); + + if ( + PreviousButton === null || + NextButton === null || + NameInput === null + ) { + return; + } + + const GardenCount = + GameSave.Gardens.length; + + const GardenIndex = + GameSave.ActiveGardenIndex; + + PreviousButton.disabled = + GardenIndex <= 0; + + NextButton.disabled = + GardenIndex >= + GardenCount - 1; + + if ( + document.activeElement !== + NameInput + ) { + NameInput.value = + GameSave.Garden.Name; + } + + NameInput.title = + "Garden " + + (GardenIndex + 1) + + " of " + + GardenCount; +} + + +function CheckAllGardenMutations( + SaveData +) { + const OriginalGardenIndex = + SaveData.ActiveGardenIndex; + + const Result = { + Attempted: false, + Changed: false + }; + + + for ( + let GardenIndex = 0; + GardenIndex < + SaveData.Gardens.length; + GardenIndex++ + ) { + SetActiveGardenIndex( + SaveData, + GardenIndex + ); + + const GardenResult = + CheckGardenMutations( + SaveData + ); + + Result.Attempted = + Result.Attempted || + GardenResult.Attempted; + + Result.Changed = + Result.Changed || + GardenResult.Changed; + } + + + SetActiveGardenIndex( + SaveData, + OriginalGardenIndex + ); + + return Result; +} + + function BindGardenTools() { const ShovelButton = document.getElementById( @@ -326,6 +550,25 @@ function RenderGarden() { GameSave.Garden.Width ); + const GardenBorderColours = [ + "var(--Lavender)", + "var(--Mauve)", + "var(--Pink)", + "var(--Blue)", + "var(--Teal)", + "var(--Peach)", + "var(--Yellow)", + "var(--Green)" + ]; + + GardenGrid.style.setProperty( + "--GardenBorderColour", + GardenBorderColours[ + GameSave.ActiveGardenIndex % + GardenBorderColours.length + ] + ); + GardenGrid.replaceChildren(); @@ -954,7 +1197,7 @@ async function GardenTick() { try { const MutationResult = - CheckGardenMutations( + CheckAllGardenMutations( GameSave ); diff --git a/Scripts/Garden/Save.js b/Scripts/Garden/Save.js index b577eec..d04063c 100644 --- a/Scripts/Garden/Save.js +++ b/Scripts/Garden/Save.js @@ -10,6 +10,357 @@ const StartingSeeds = { }; +const DefaultGardenName = "Garden"; +const MaximumGardenNameLength = 32; +const StartingGardenWidth = 3; +const StartingGardenHeight = 3; + + +function CreateGardenData( + Name = DefaultGardenName +) { + return { + Name: NormalizeGardenName( + Name + ), + Width: StartingGardenWidth, + Height: StartingGardenHeight, + Plots: Array( + StartingGardenWidth * + StartingGardenHeight + ).fill(null), + MutationCooldowns: {} + }; +} + + +function NormalizeGardenName( + Value +) { + if ( + typeof Value !== "string" + ) { + return DefaultGardenName; + } + + const Name = Value + .trim() + .slice( + 0, + MaximumGardenNameLength + ); + + return Name.length > 0 + ? Name + : DefaultGardenName; +} + + +function NormalizeGardenData( + Garden, + LegacyMutationCooldowns = null +) { + if ( + Garden === null || + typeof Garden !== "object" || + Array.isArray(Garden) + ) { + Garden = CreateGardenData(); + } + + Garden.Name = NormalizeGardenName( + Garden.Name + ); + + Garden.Width = Math.max( + 1, + Math.floor( + Number( + Garden.Width ?? + StartingGardenWidth + ) || StartingGardenWidth + ) + ); + + Garden.Height = Math.max( + 1, + Math.floor( + Number( + Garden.Height ?? + StartingGardenHeight + ) || StartingGardenHeight + ) + ); + + if (!Array.isArray(Garden.Plots)) { + Garden.Plots = []; + } + + + for ( + let PlotIndex = 0; + PlotIndex < Garden.Plots.length; + PlotIndex++ + ) { + const Plot = + Garden.Plots[PlotIndex]; + + if (Plot === null) { + continue; + } + + if ( + typeof Plot !== "object" || + Array.isArray(Plot) + ) { + Garden.Plots[PlotIndex] = null; + continue; + } + + if ( + Plot.Plant === "Rose" + ) { + Plot.Plant = "RedRose"; + } + + if ( + typeof Plot.Plant === + "number" + ) { + const PlantKey = + ResolvePlantKeyById( + Plot.Plant + ); + + if (PlantKey !== null) { + Plot.Plant = PlantKey; + } + } + + Plot.PlantedAt = Number( + Plot.PlantedAt ?? + Date.now() + ); + + delete Plot.VisualVariant; + delete Plot.AddedTags; + } + + + const RequiredPlots = + Garden.Width * + Garden.Height; + + while ( + Garden.Plots.length < + RequiredPlots + ) { + Garden.Plots.push(null); + } + + if ( + Garden.Plots.length > + RequiredPlots + ) { + Garden.Plots.length = + RequiredPlots; + } + + + if ( + Garden.MutationCooldowns === + undefined && + LegacyMutationCooldowns !== null && + typeof LegacyMutationCooldowns === + "object" && + !Array.isArray( + LegacyMutationCooldowns + ) + ) { + Garden.MutationCooldowns = + LegacyMutationCooldowns; + } + + if ( + Garden.MutationCooldowns === null || + typeof Garden.MutationCooldowns !== + "object" || + Array.isArray( + Garden.MutationCooldowns + ) + ) { + Garden.MutationCooldowns = {}; + } + + + return Garden; +} + + +function GetActiveGarden( + SaveData +) { + if ( + !Array.isArray( + SaveData.Gardens + ) || + SaveData.Gardens.length === 0 + ) { + SaveData.Gardens = [ + CreateGardenData() + ]; + } + + const GardenIndex = Math.min( + SaveData.Gardens.length - 1, + Math.max( + 0, + Math.floor( + Number( + SaveData.ActiveGardenIndex ?? + 0 + ) || 0 + ) + ) + ); + + SaveData.ActiveGardenIndex = + GardenIndex; + + return SaveData.Gardens[ + GardenIndex + ]; +} + + +function SetActiveGardenIndex( + SaveData, + GardenIndex +) { + if ( + !Array.isArray( + SaveData.Gardens + ) || + SaveData.Gardens.length === 0 + ) { + return false; + } + + const NormalizedIndex = Math.min( + SaveData.Gardens.length - 1, + Math.max( + 0, + Math.floor( + Number( + GardenIndex + ) || 0 + ) + ) + ); + + const Changed = + SaveData.ActiveGardenIndex !== + NormalizedIndex; + + SaveData.ActiveGardenIndex = + NormalizedIndex; + + return Changed; +} + + +function RenameGarden( + SaveData, + GardenIndex, + Name +) { + if ( + !Array.isArray( + SaveData.Gardens + ) || + SaveData.Gardens[GardenIndex] === + undefined + ) { + return DefaultGardenName; + } + + const NormalizedName = + NormalizeGardenName( + Name + ); + + SaveData.Gardens[ + GardenIndex + ].Name = NormalizedName; + + return NormalizedName; +} + + +function DefineActiveGardenAliases( + SaveData +) { + delete SaveData.Garden; + delete SaveData.MutationCooldowns; + + Object.defineProperty( + SaveData, + "Garden", + { + configurable: true, + enumerable: false, + + get() { + return GetActiveGarden( + SaveData + ); + }, + + set(Value) { + const GardenIndex = + SaveData.ActiveGardenIndex ?? + 0; + + SaveData.Gardens[ + GardenIndex + ] = NormalizeGardenData( + Value + ); + } + } + ); + + Object.defineProperty( + SaveData, + "MutationCooldowns", + { + configurable: true, + enumerable: false, + + get() { + return GetActiveGarden( + SaveData + ).MutationCooldowns; + }, + + set(Value) { + const Garden = + GetActiveGarden( + SaveData + ); + + Garden.MutationCooldowns = + Value !== null && + typeof Value === "object" && + !Array.isArray(Value) + ? Value + : {}; + } + } + ); +} + + function CreateNewSave() { const SeedInventory = {}; const StartingPlants = []; @@ -40,8 +391,8 @@ function CreateNewSave() { } - return { - Version: 2, + return NormalizeSaveData({ + Version: 3, Revision: 0, LastSavedAt: Date.now(), @@ -83,19 +434,17 @@ function CreateNewSave() { ShowReadyPlots: true }, - Garden: { - Width: 3, - Height: 3, - Plots: Array(9).fill(null) - }, + Gardens: [ + CreateGardenData() + ], + + ActiveGardenIndex: 0, Discoveries: { Plants: StartingPlants, Mutations: [] - }, - - MutationCooldowns: {} - }; + } + }); } @@ -106,7 +455,7 @@ function NormalizeSaveData( Number( SaveData.Version ?? 1 ), - 2 + 3 ); SaveData.Revision = Number( @@ -301,100 +650,48 @@ function NormalizeSaveData( false; - SaveData.Garden ??= { - Width: 3, - Height: 3, - Plots: [] - }; + const LegacyGarden = + SaveData.Garden ?? + null; - SaveData.Garden.Width = Math.max( - 1, - Math.floor( - Number( - SaveData.Garden.Width ?? 3 - ) || 3 - ) - ); + const LegacyMutationCooldowns = + SaveData.MutationCooldowns ?? + null; - SaveData.Garden.Height = Math.max( - 1, - Math.floor( - Number( - SaveData.Garden.Height ?? 3 - ) || 3 - ) - ); - - SaveData.Garden.Plots ??= []; - - - for ( - let PlotIndex = 0; - PlotIndex < - SaveData.Garden.Plots.length; - PlotIndex++ - ) { - const Plot = - SaveData.Garden.Plots[ - PlotIndex - ]; - - if (Plot === null) { - continue; - } - - if ( - Plot.Plant === "Rose" - ) { - Plot.Plant = - "RedRose"; - } - - if ( - typeof Plot.Plant === - "number" - ) { - const PlantKey = - ResolvePlantKeyById( - Plot.Plant - ); - - if (PlantKey !== null) { - Plot.Plant = PlantKey; - } - } - - Plot.PlantedAt = Number( - Plot.PlantedAt ?? - Date.now() - ); - - delete Plot.VisualVariant; - delete Plot.AddedTags; - } - - - const RequiredPlots = - SaveData.Garden.Width * - SaveData.Garden.Height; - - while ( - SaveData.Garden.Plots.length < - RequiredPlots - ) { - SaveData.Garden.Plots.push(null); - } if ( - SaveData.Garden.Plots.length > - RequiredPlots + !Array.isArray( + SaveData.Gardens + ) || + SaveData.Gardens.length === 0 ) { - SaveData.Garden.Plots.length = - RequiredPlots; + SaveData.Gardens = [ + LegacyGarden ?? + CreateGardenData() + ]; } - SaveData.MutationCooldowns ??= {}; + SaveData.Gardens = + SaveData.Gardens.map( + (Garden, GardenIndex) => + NormalizeGardenData( + Garden, + GardenIndex === 0 + ? LegacyMutationCooldowns + : null + ) + ); + + + SetActiveGardenIndex( + SaveData, + SaveData.ActiveGardenIndex ?? 0 + ); + + DefineActiveGardenAliases( + SaveData + ); return SaveData; diff --git a/Scripts/Garden/Shop.js b/Scripts/Garden/Shop.js index 23ac82e..dd66dc5 100644 --- a/Scripts/Garden/Shop.js +++ b/Scripts/Garden/Shop.js @@ -124,6 +124,7 @@ function RenderGardenUpgrades() { CreateGardenExpansionCard( "Row" ), + CreateNewGardenCard(), CreatePlantInformationUpgradeCard(), CreateGardenOverviewUpgradeCard(), CreateMutationHintsUpgradeCard() @@ -210,6 +211,10 @@ function CreateGardenExpansionCard( "ShopItemDetails"; Details.append( + CreateShopStat( + "Garden", + ShopSave.Garden.Name + ), CreateShopStat( "Current size", ShopSave.Garden.Width + @@ -273,6 +278,115 @@ function CreateGardenExpansionCard( } +function CreateNewGardenCard() { + const Cost = + GetNewGardenCost( + ShopSave + ); + + const GardensOwned = + ShopSave.Gardens.length; + + + const Card = + document.createElement( + "article" + ); + + Card.className = + "Panel ShopItem"; + + + const Header = + document.createElement( + "div" + ); + + Header.className = + "ShopItemHeader"; + + + const Name = + document.createElement( + "h3" + ); + + Name.textContent = + "Buy new Garden"; + + Header.appendChild(Name); + + + const Description = + document.createElement( + "p" + ); + + Description.textContent = + "Buy another independent 3×3 Garden. New Gardens can be named and expanded separately."; + + + const Details = + document.createElement( + "div" + ); + + Details.className = + "ShopItemDetails"; + + Details.append( + CreateShopStat( + "Gardens owned", + GardensOwned.toLocaleString() + ), + CreateShopStat( + "New Garden size", + "3×3 (9 plots)" + ), + CreateShopStat( + "Price scaling", + "+50% per Garden" + ) + ); + + + const BuyButton = + document.createElement( + "button" + ); + + BuyButton.className = + "ActionButton ShopBuyButton"; + + BuyButton.type = "button"; + + BuyButton.textContent = + "Buy - " + + Cost.toLocaleString() + + " Dew"; + + BuyButton.disabled = + ShopSave.Currency.Dew < Cost || + ShopPurchasePending; + + BuyButton.addEventListener( + "click", + BuyNewGarden + ); + + + Card.append( + Header, + Description, + Details, + BuyButton + ); + + + return Card; +} + + function CreatePlantInformationUpgradeCard() { const Cost = GetPlantInformationUpgradeCost(); @@ -1188,6 +1302,60 @@ async function BuyGardenExpansion( } +async function BuyNewGarden() { + if (ShopPurchasePending) { + return; + } + + + const Cost = + GetNewGardenCost( + ShopSave + ); + + if ( + ShopSave.Currency.Dew < + Cost + ) { + SetShopMessage( + "You don't have enough Dew for a new Garden." + ); + + return; + } + + + ShopPurchasePending = true; + + try { + ShopSave.Currency.Dew -= + Cost; + + ShopSave.Statistics.CurrencySpent.Dew += + Cost; + + AddNewGarden( + ShopSave + ); + + SetShopMessage( + "Bought a new 3×3 Garden for " + + Cost.toLocaleString() + + " Dew. It is now your active Garden." + ); + + RenderShop(); + + await SaveGame( + ShopSave + ); + } finally { + ShopPurchasePending = false; + RenderShop(); + } +} + + async function BuyPlantInformationUpgrade() { if ( ShopPurchasePending || diff --git a/Scripts/Garden/Upgrades.js b/Scripts/Garden/Upgrades.js index f725421..5537527 100644 --- a/Scripts/Garden/Upgrades.js +++ b/Scripts/Garden/Upgrades.js @@ -2,6 +2,9 @@ const GardenExpansionBaseCost = 1000; const PlantInformationUpgradeCost = 500; const GardenOverviewUpgradeCost = 1000; const MutationHintsUpgradeCost = 1500; +const NewGardenBaseCost = 30000; +const NewGardenCostMultiplier = 1.5; +const NewGardenCostRounding = 1000; function GetGardenColumnUpgradeCost( @@ -112,6 +115,51 @@ function ResetGardenMutationCooldowns( } +function GetNewGardenCost( + SaveData +) { + const GardensOwned = Math.max( + 1, + SaveData.Gardens?.length ?? 1 + ); + + let Cost = + NewGardenBaseCost; + + for ( + let GardenNumber = 1; + GardenNumber < GardensOwned; + GardenNumber++ + ) { + Cost = Math.round( + Cost * + NewGardenCostMultiplier / + NewGardenCostRounding + ) * NewGardenCostRounding; + } + + return Cost; +} + + +function AddNewGarden( + SaveData +) { + SaveData.Gardens ??= []; + + SaveData.Gardens.push( + CreateGardenData() + ); + + SetActiveGardenIndex( + SaveData, + SaveData.Gardens.length - 1 + ); + + return SaveData.Garden; +} + + function HasPlantInformationUpgrade( SaveData ) { diff --git a/Styles/Style.css b/Styles/Style.css index 8599e77..887b08b 100644 --- a/Styles/Style.css +++ b/Styles/Style.css @@ -313,6 +313,45 @@ input, button, textarea, select { image-rendering: auto; } +.GardenSelectorPanel { + display: grid; + grid-template-columns: 3rem minmax(0, 1fr) 3rem; + gap: 0.5rem; + margin-bottom: 1rem; + padding: 0.5rem; + background-color: var(--Mantle); + border: 3px solid var(--Surface2); +} + +.GardenSelectorButton { + width: 100%; + min-width: 0; + padding-right: 0; + padding-left: 0; + font-size: 1.25rem; +} + +.GardenNameInput { + width: 100%; + min-width: 0; + padding: 0.6rem 0.75rem; + color: var(--Text); + background-color: var(--Surface0); + border: 3px solid var(--Surface2); + border-radius: 0; + text-align: center; + box-shadow: 3px 3px 0 var(--Crust); +} + +.GardenNameInput:hover { + border-color: var(--Sky); +} + +.GardenNameInput:focus { + outline: 3px solid var(--Yellow); + outline-offset: 3px; +} + .GardenLayout { display: grid; grid-template-columns: minmax(0, 1fr) 12rem; @@ -327,7 +366,7 @@ input, button, textarea, select { gap: 0.5rem; padding: 0.75rem; background-color: var(--Mantle); - border: 4px solid var(--Lavender); + border: 4px solid var(--GardenBorderColour, var(--Lavender)); } .GardenPlot {