From a055a7414889d26de3f6b98bef932ee9f7e2a7b0 Mon Sep 17 00:00:00 2001 From: Sarahtonin Date: Thu, 3 Sep 2026 21:00:30 +0200 Subject: [PATCH] Added quick-buy and bulk-buying qol features to both shop and garden. --- Pages/Garden.php | 44 ++++ Scripts/Garden/Garden.js | 452 +++++++++++++++++++++++++++++++++++++++ Scripts/Garden/Shop.js | 190 +++++++++++----- Styles/Style.css | 141 ++++++++++++ 4 files changed, 769 insertions(+), 58 deletions(-) diff --git a/Pages/Garden.php b/Pages/Garden.php index 800b4e2..aaf2359 100644 --- a/Pages/Garden.php +++ b/Pages/Garden.php @@ -164,6 +164,50 @@ $PageSection = "Garden"; +
+
+

Quick-buy

+ +
+ + + + + +
+
+ +
+
+
{ + QuickBuyAmountMode = + Mode; + + RenderQuickBuy(); + } + ); + } +} + + +function GetQuickBuyAmount() { + if ( + QuickBuyAmountMode === + "Two" + ) { + return 2; + } + + if ( + QuickBuyAmountMode === + "Garden" + ) { + return Math.max( + 1, + GameSave.Garden.Width * + GameSave.Garden.Height + ); + } + + return 1; +} + + +function RenderQuickBuy() { + const PlantList = + document.getElementById( + "QuickBuyPlantList" + ); + + if (PlantList === null) { + return; + } + + + const Amount = + GetQuickBuyAmount(); + + const OneButton = + document.getElementById( + "QuickBuyOneButton" + ); + + const TwoButton = + document.getElementById( + "QuickBuyTwoButton" + ); + + const GardenButton = + document.getElementById( + "QuickBuyGardenButton" + ); + + + const AmountButtons = [ + [OneButton, "One", 1], + [TwoButton, "Two", 2], + [ + GardenButton, + "Garden", + Math.max( + 1, + GameSave.Garden.Width * + GameSave.Garden.Height + ) + ] + ]; + + + for ( + const [Button, Mode, ButtonAmount] + of AmountButtons + ) { + if (Button === null) { + continue; + } + + Button.textContent = + "×" + + ButtonAmount.toLocaleString(); + + Button.disabled = + QuickBuyPurchasePending; + + if ( + QuickBuyAmountMode === + Mode + ) { + Button.setAttribute( + "aria-current", + "true" + ); + } else { + Button.removeAttribute( + "aria-current" + ); + } + } + + + PlantList.replaceChildren(); + + + const AvailablePlants = + Object.values(Plants) + .filter( + Plant => { + if ( + !IsPlantAvailableInShop( + GameSave, + Plant + ) + ) { + return false; + } + + const Cost = + GetPlantShopCost( + GameSave, + Plant.Id + ); + + return ( + Cost !== null && + GameSave.Currency.Dew >= + Cost * Amount + ); + } + ) + .sort( + (A, B) => + A.Id - B.Id + ); + + + if ( + AvailablePlants.length === 0 + ) { + const EmptyMessage = + document.createElement( + "p" + ); + + EmptyMessage.className = + "QuickBuyEmpty"; + + EmptyMessage.textContent = + Amount === 1 + ? "You can't afford any available seeds." + : "You can't afford this many of any available seed."; + + PlantList.appendChild( + EmptyMessage + ); + + return; + } + + + for ( + const Plant + of AvailablePlants + ) { + PlantList.appendChild( + CreateQuickBuyPlantButton( + Plant, + Amount + ) + ); + } +} + + +function CreateQuickBuyPlantButton( + Plant, + Amount +) { + const UnitCost = + GetPlantShopCost( + GameSave, + Plant.Id + ); + + const TotalCost = + UnitCost * Amount; + + + const Button = + document.createElement( + "button" + ); + + Button.className = + "PlantTile QuickBuyPlantButton"; + + Button.type = "button"; + + Button.disabled = + QuickBuyPurchasePending; + + Button.title = + "Buy " + + Amount.toLocaleString() + + (Amount === 1 + ? " seed of " + : " seeds of ") + + Plant.Name + + " for " + + TotalCost.toLocaleString() + + " Dew"; + + + const Name = + document.createElement( + "span" + ); + + Name.className = + "QuickBuyPlantName"; + + Name.textContent = + Plant.Name; + + + const Visual = + document.createElement( + "span" + ); + + Visual.className = + "QuickBuyPlantVisual"; + + + const MatureImage = + GetPlantMatureImageSource( + Plant + ); + + + if (MatureImage === null) { + const Missing = + document.createElement( + "span" + ); + + Missing.className = + "QuickBuyPlantMissing"; + + Missing.textContent = + "No image"; + + Visual.appendChild( + Missing + ); + } else { + const Image = + document.createElement( + "img" + ); + + Image.className = + "PlantSprite"; + + Image.src = + MatureImage; + + Image.alt = + Plant.Name; + + Visual.appendChild( + Image + ); + } + + + Button.append( + Name, + Visual + ); + + + Button.addEventListener( + "click", + () => { + BuyQuickBuyPlantSeed( + Plant.Id + ); + } + ); + + + return Button; +} + + +async function BuyQuickBuyPlantSeed( + PlantId +) { + if (QuickBuyPurchasePending) { + return; + } + + + const Plant = + GetPlantById( + PlantId + ); + + if ( + Plant === null || + !IsPlantAvailableInShop( + GameSave, + Plant + ) + ) { + return; + } + + + const Amount = + GetQuickBuyAmount(); + + const UnitCost = + GetPlantShopCost( + GameSave, + PlantId + ); + + if (UnitCost === null) { + SetGardenMessage( + "That seed doesn't have a purchasable recipe yet." + ); + + return; + } + + + const TotalCost = + UnitCost * Amount; + + if ( + GameSave.Currency.Dew < + TotalCost + ) { + SetGardenMessage( + "You don't have enough Dew for " + + Amount.toLocaleString() + + (Amount === 1 + ? " seed of " + : " seeds of ") + + Plant.Name + + "." + ); + + RenderQuickBuy(); + + return; + } + + + QuickBuyPurchasePending = true; + + try { + GameSave.Currency.Dew -= + TotalCost; + + GameSave.Statistics.CurrencySpent.Dew += + TotalCost; + + GameSave.Statistics.SeedsPurchased += + Amount; + + AddSeed( + GameSave, + PlantId, + Amount + ); + + DiscoverPlant( + GameSave, + PlantId + ); + + + SetGardenMessage( + "Bought " + + Amount.toLocaleString() + + " " + + Plant.Name + + (Amount === 1 + ? " seed for " + : " seeds for ") + + TotalCost.toLocaleString() + + " Dew." + ); + + RenderGame(); + + await SaveGame( + GameSave + ); + } finally { + QuickBuyPurchasePending = + false; + + RenderGame(); + } +} + + function RenderCurrency() { const DewAmount = document.getElementById( diff --git a/Scripts/Garden/Shop.js b/Scripts/Garden/Shop.js index 1598e26..40c2fe1 100644 --- a/Scripts/Garden/Shop.js +++ b/Scripts/Garden/Shop.js @@ -1001,43 +1001,95 @@ function CreateShopSeedCard( ); - const BuyButton = + const BuyOptions = document.createElement( - "button" + "div" ); - BuyButton.className = - "ActionButton ShopBuyButton"; + BuyOptions.className = + "ShopSeedBuyOptions"; - BuyButton.type = "button"; - BuyButton.textContent = - Cost === null - ? "Unavailable" - : "Buy seed - " + - Cost.toLocaleString() + - " Dew"; + const GardenSize = + Math.max( + 1, + ShopSave.Garden.Width * + ShopSave.Garden.Height + ); - BuyButton.disabled = - Cost === null || - ShopSave.Currency.Dew < - Cost || - ShopPurchasePending; - - BuyButton.addEventListener( - "click", - () => { - BuyPlantSeed( - Plant.Id - ); + const PurchaseOptions = [ + { + Amount: 1, + Label: "Buy seed" + }, + { + Amount: 10, + Label: "Buy 10 seeds" + }, + { + Amount: GardenSize, + Label: + "Buy " + + GardenSize.toLocaleString() + + " seeds" } - ); + ]; + + + for ( + const Option + of PurchaseOptions + ) { + const BuyButton = + document.createElement( + "button" + ); + + BuyButton.className = + "ActionButton ShopBuyButton"; + + BuyButton.type = "button"; + + + const TotalCost = + Cost === null + ? null + : Cost * Option.Amount; + + BuyButton.textContent = + TotalCost === null + ? "Unavailable" + : Option.Label + + " - " + + TotalCost.toLocaleString() + + " Dew"; + + BuyButton.disabled = + TotalCost === null || + ShopSave.Currency.Dew < + TotalCost || + ShopPurchasePending; + + BuyButton.addEventListener( + "click", + () => { + BuyPlantSeed( + Plant.Id, + Option.Amount + ); + } + ); + + BuyOptions.appendChild( + BuyButton + ); + } Card.append( Header, Body, - BuyButton + BuyOptions ); @@ -1225,13 +1277,22 @@ function CreateShopStat( async function BuyPlantSeed( - PlantId + PlantId, + Amount = 1 ) { if (ShopPurchasePending) { return; } + Amount = Math.max( + 1, + Math.floor( + Number(Amount) || 1 + ) + ); + + const Plant = GetPlantById( PlantId @@ -1248,13 +1309,13 @@ async function BuyPlantSeed( } - const Cost = + const UnitCost = GetPlantShopCost( ShopSave, PlantId ); - if (Cost === null) { + if (UnitCost === null) { SetShopMessage( "That seed doesn't have a purchasable recipe yet." ); @@ -1263,12 +1324,19 @@ async function BuyPlantSeed( } + const TotalCost = + UnitCost * Amount; + if ( ShopSave.Currency.Dew < - Cost + TotalCost ) { SetShopMessage( "You don't have enough Dew for " + + Amount.toLocaleString() + + (Amount === 1 + ? " seed of " + : " seeds of ") + Plant.Name + "." ); @@ -1279,45 +1347,51 @@ async function BuyPlantSeed( ShopPurchasePending = true; - ShopSave.Currency.Dew -= - Cost; + try { + ShopSave.Currency.Dew -= + TotalCost; - ShopSave.Statistics.CurrencySpent.Dew += - Cost; + ShopSave.Statistics.CurrencySpent.Dew += + TotalCost; - ShopSave.Statistics.SeedsPurchased++; + ShopSave.Statistics.SeedsPurchased += + Amount; - AddSeed( - ShopSave, - PlantId - ); + AddSeed( + ShopSave, + PlantId, + Amount + ); - DiscoverPlant( - ShopSave, - PlantId - ); - - SetShopMessage( - "Bought a " + - Plant.Name + - " seed for " + - Cost + - " Dew." - ); - - RenderShop(); + DiscoverPlant( + ShopSave, + PlantId + ); - await SaveGame( - ShopSave - ); + SetShopMessage( + "Bought " + + Amount.toLocaleString() + + " " + + Plant.Name + + (Amount === 1 + ? " seed for " + : " seeds for ") + + TotalCost.toLocaleString() + + " Dew." + ); + RenderShop(); - ShopPurchasePending = false; - RenderShop(); + await SaveGame( + ShopSave + ); + } finally { + ShopPurchasePending = false; + RenderShop(); + } } - async function BuyGardenExpansion( Direction ) { diff --git a/Styles/Style.css b/Styles/Style.css index ece30de..9c7a2ad 100644 --- a/Styles/Style.css +++ b/Styles/Style.css @@ -415,6 +415,140 @@ input, button, textarea, select { } } +.GardenQuickBuyPanel { + margin-bottom: 1rem; + padding: 0.75rem; +} + +.GardenQuickBuyHeader { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.GardenQuickBuyHeader h2 { + margin: 0; +} + +.QuickBuyAmountList { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.QuickBuyAmountButton { + min-width: 3.25rem; + padding: 0.4rem 0.55rem; + cursor: pointer; + color: var(--Text); + background-color: var(--Surface0); + border: 3px solid var(--Surface2); + border-radius: 0; + box-shadow: 2px 2px 0 var(--Crust); + appearance: none; +} + +.QuickBuyAmountButton:hover:not(:disabled) { + color: var(--Green); + border-color: var(--Green); +} + +.QuickBuyAmountButton[aria-current="true"] { + color: var(--Green); + background-color: var(--Surface1); + border-color: var(--Green); +} + +.QuickBuyAmountButton:disabled { + cursor: not-allowed; +} + +.QuickBuyAmountButton:focus-visible, +.QuickBuyPlantButton:focus-visible { + outline: 3px solid var(--Yellow); + outline-offset: 3px; +} + +.QuickBuyPlantList { + display: flex; + gap: 0.65rem; + padding-bottom: 0.45rem; + overflow-x: auto; + overflow-y: hidden; +} + +.QuickBuyPlantButton { + display: flex; + flex: 0 0 7rem; + width: 7rem; + padding: 0; + flex-direction: column; + cursor: pointer; + color: var(--Text); + border-color: var(--Surface2); + border-radius: 0; + box-shadow: 3px 3px 0 var(--Crust); + appearance: none; +} + +.QuickBuyPlantButton:hover:not(:disabled) { + border-color: var(--Green); + transform: translate(1px, 1px); + box-shadow: 2px 2px 0 var(--Crust); +} + +.QuickBuyPlantButton:disabled { + cursor: not-allowed; +} + +.QuickBuyPlantName { + display: flex; + width: 100%; + min-width: 0; + min-height: 1.7rem; + padding: 0.25rem 0.35rem; + align-items: center; + justify-content: center; + overflow: hidden; + color: var(--Subtext1); + background-color: var(--Mantle); + border-bottom: 2px solid var(--Surface2); + font-size: 0.75rem; + line-height: 1; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.QuickBuyPlantVisual { + display: flex; + width: 100%; + aspect-ratio: 1; + align-items: center; + justify-content: center; + overflow: hidden; + background-color: var(--Surface0); +} + +.QuickBuyPlantMissing { + padding: 0.35rem; + color: var(--Overlay1); + font-size: 0.75rem; + text-align: center; +} + +.QuickBuyEmpty { + flex: 0 0 100%; + margin: 0; + padding: 0.75rem; + color: var(--Subtext1); + background-color: var(--Mantle); + border: 3px solid var(--Surface2); +} + .GardenLayout { display: grid; grid-template-columns: minmax(0, 1fr) 12rem; @@ -1077,6 +1211,13 @@ input, button, textarea, select { padding: 0.65rem 0.8rem; } +.ShopSeedBuyOptions { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: auto; +} + .ShopBuyButton:disabled { cursor: not-allowed; color: var(--Overlay1);