From 7b804feba843a5d7f3ed3ea35d7c2a94157f60bb Mon Sep 17 00:00:00 2001 From: Sarahtonin Date: Sat, 5 Sep 2026 11:02:21 +0200 Subject: [PATCH] Added tools and tool upgrades. --- Pages/Garden.php | 15 +- Pages/Guide.php | 42 +- Pages/Profile.php | 23 + Pages/Shop.php | 33 ++ Scripts/Garden/Garden.js | 842 +++++++++++++++++++++++++++++-- Scripts/Garden/Profile.js | 57 +++ Scripts/Garden/Save.js | 85 +++- Scripts/Garden/Shop.js | 982 +++++++++++++++++++++++++++++++++++++ Scripts/Garden/Upgrades.js | 342 +++++++++++++ Styles/Style.css | 66 +++ 10 files changed, 2418 insertions(+), 69 deletions(-) diff --git a/Pages/Garden.php b/Pages/Garden.php index 42f5d25..5aa6db8 100644 --- a/Pages/Garden.php +++ b/Pages/Garden.php @@ -216,15 +216,10 @@ $PageSection = "Garden"; >

Tools

-
- -
+

- Select a seed, then choose an empty plot. + Select a seed and use the Trowel on an empty plot.

Tools

- The Shovel removes a planted crop without giving a - harvest reward. Select it, click the plant you want - to remove, and click the Shovel again when you want - to have no tool equipped. It's very useful when you - decide you want to try a new mutation, for example. + The Trowel is the default tool. Select + a seed from your inventory, then use the Trowel on an + empty plot to plant it. +

+ +

+ The Magic trowel fills as many empty + plots as it can with the selected seed. If you have + fewer seeds than empty plots, it simply plants every + seed you have. +

+ +

+ The Shovel removes planted crops. At + level 0 it returns nothing, level 1 returns 33% of the + seed cost, level 2 returns 66%, and level 3 returns the + planted seed itself. +

+ +

+ Fertilizer consumes one Fertilizer use + to advance a growing plant. Its six levels advance 10, + 20, 30, 40, 50, or 60 minutes respectively. Additional + Fertilizer uses are bought from the Shop. +

+ +

+ Future Sight previews the Garden's next + mutation as though that mutation succeeded. The preview + does not actually mutate or discover anything, and Future + Sight must recharge for two hours after a successful use. +

+ +

+ Tools and their upgrades are bought from the Shop. Shovel + and Fertilizer upgrade prices rise exponentially as their + levels increase.

diff --git a/Pages/Profile.php b/Pages/Profile.php index 0dbe0fe..7b65463 100644 --- a/Pages/Profile.php +++ b/Pages/Profile.php @@ -105,6 +105,29 @@ $PageTitle = "Profile"; >

Garden display

+

Tool selection

+ +

+ Choose which Garden actions automatically change + your selected seed or tool. +

+ +
+ + + +
+

Plant information

diff --git a/Pages/Shop.php b/Pages/Shop.php index a42130f..30b7d97 100644 --- a/Pages/Shop.php +++ b/Pages/Shop.php @@ -40,6 +40,39 @@ $PageSection = "Shop"; seed price, rounded up.

+
+

+ +

+ +
+

+ Unlock Garden tools, improve the Shovel and + Fertilizer, and restock Fertilizer uses. +

+ +
+
+
+
{ + Event => { + const Button = + Event.target.closest( + "[data-garden-tool]" + ); + if ( - SelectedGardenTool === - "Shovel" + Button === null || + !ToolList.contains( + Button + ) ) { - SelectedGardenTool = - null; - } else { - SelectedGardenTool = - "Shovel"; + return; } - RenderSeeds(); - RenderTools(); - RenderGarden(); + const Tool = + Button.dataset + .gardenTool; + + SelectGardenTool( + Tool + ); } ); } -function RenderTools() { - const ShovelButton = - document.getElementById( - "ShovelToolButton" - ); - - if (ShovelButton === null) { +async function SelectGardenTool( + Tool +) { + if ( + !IsGardenToolUnlocked( + GameSave, + Tool + ) + ) { return; } + + if (Tool === "FutureSight") { + if ( + !IsFutureSightReady( + GameSave + ) && + FutureSightPreview === null + ) { + SetGardenMessage( + "Future Sight is still recharging for " + + FormatGardenRemainingTime( + GetFutureSightCooldownRemaining( + GameSave + ) + ) + + "." + ); + + RenderTools(); + + return; + } + + SelectedGardenTool = + "FutureSight"; + + if (FutureSightPreview === null) { + await UseFutureSight(); + } else { + RenderTools(); + RenderGarden(); + } + + return; + } + + + SelectedGardenTool = Tool; + FutureSightPreview = null; + + RenderSeeds(); + RenderTools(); + RenderGarden(); +} + + +function EnsureSelectedGardenTool() { if ( - SelectedGardenTool === - "Shovel" + !IsGardenToolUnlocked( + GameSave, + SelectedGardenTool + ) ) { - ShovelButton.setAttribute( - "aria-current", - "true" + SelectedGardenTool = + "Trowel"; + + FutureSightPreview = null; + } +} + + +function RenderTools() { + const ToolList = + document.getElementById( + "ToolList" ); - } else { - ShovelButton.removeAttribute( - "aria-current" + + if (ToolList === null) { + return; + } + + + EnsureSelectedGardenTool(); + + const ToolDefinitions = [ + [ + "Trowel", + "Trowel" + ], + [ + "MagicTrowel", + "Magic trowel" + ], + [ + "Shovel", + "Shovel" + ], + [ + "Fertilizer", + "Fertilizer" + ], + [ + "FutureSight", + "Future Sight" + ] + ]; + + + ToolList.replaceChildren(); + + + for ( + const [Tool, Name] + of ToolDefinitions + ) { + if ( + !IsGardenToolUnlocked( + GameSave, + Tool + ) + ) { + continue; + } + + + const Button = + document.createElement( + "button" + ); + + Button.className = + "ActionButton ToolButton ToolButton" + + Tool; + + Button.type = "button"; + + Button.dataset.gardenTool = + Tool; + + + let Label = Name; + + if (Tool === "Fertilizer") { + Label += + " ×" + + Number( + GameSave.Inventory + .Fertilizer ?? 0 + ).toLocaleString(); + } + + if (Tool === "FutureSight") { + const Remaining = + GetFutureSightCooldownRemaining( + GameSave + ); + + if (Remaining > 0) { + Label += + " (" + + FormatGardenRemainingTime( + Remaining + ) + + ")"; + } + + Button.disabled = + Remaining > 0 && + !( + SelectedGardenTool === + "FutureSight" && + FutureSightPreview !== + null + ); + } + + + Button.textContent = Label; + + + if ( + SelectedGardenTool === + Tool + ) { + Button.setAttribute( + "aria-current", + "true" + ); + } + + + ToolList.appendChild( + Button ); } } @@ -861,6 +1055,16 @@ async function BuyQuickBuyPlantSeed( PlantId ); + if ( + GameSave.Preferences + .SelectQuickBoughtPlant !== + false + ) { + SelectInventoryPlant( + PlantId + ); + } + SetGardenMessage( "Bought " + @@ -938,12 +1142,41 @@ function EnsureSelectedSeed() { return; } +} + + +function SelectInventoryPlant( + PlantId +) { if ( - SelectedGardenTool === - "Seed" + GetPlantById( + PlantId + ) === null || + GetSeedCount( + GameSave, + PlantId + ) <= 0 ) { - SelectedGardenTool = null; + return false; } + + + SelectedSeedId = + Number(PlantId); + + if ( + GameSave.Preferences + .SelectTrowelWithInventoryPlant !== + false + ) { + SelectedGardenTool = + "Trowel"; + + FutureSightPreview = null; + } + + + return true; } @@ -1043,10 +1276,8 @@ function RenderSeeds() { ); if ( - SelectedGardenTool === - "Seed" && PlantId === - SelectedSeedId + SelectedSeedId ) { Button.setAttribute( "aria-current", @@ -1057,11 +1288,9 @@ function RenderSeeds() { Button.addEventListener( "click", () => { - SelectedGardenTool = - "Seed"; - - SelectedSeedId = - PlantId; + SelectInventoryPlant( + PlantId + ); RenderSeeds(); RenderTools(); @@ -1105,22 +1334,58 @@ function RenderGarden() { ) ); + GardenPanel.classList.toggle( + "GardenPlotsFutureSight", + SelectedGardenTool === + "FutureSight" && + FutureSightPreview !== null + ); + GardenGrid.replaceChildren(); const DisplaySettings = GetGardenDisplaySettings(); + const IsFutureSightPreview = + SelectedGardenTool === + "FutureSight" && + FutureSightPreview !== null; + GameSave.Garden.Plots.forEach( (Plot, PlotIndex) => { + const DisplayPlot = + IsFutureSightPreview + ? FutureSightPreview + .Plots[PlotIndex] + : Plot; + const PlotElement = CreatePlotElement( - Plot, + DisplayPlot, PlotIndex, - DisplaySettings + DisplaySettings, + IsFutureSightPreview ); + if (IsFutureSightPreview) { + PlotElement.classList.add( + "GardenPlotFutureSight" + ); + + if ( + !AreGardenPlotsEqual( + Plot, + DisplayPlot + ) + ) { + PlotElement.classList.add( + "GardenPlotFutureSightChanged" + ); + } + } + GardenGrid.appendChild( PlotElement ); @@ -1129,6 +1394,22 @@ function RenderGarden() { } +function AreGardenPlotsEqual( + A, + B +) { + if (A === null || B === null) { + return A === B; + } + + return ( + A.Plant === B.Plant && + Number(A.PlantedAt) === + Number(B.PlantedAt) + ); +} + + function GetGardenDisplaySettings() { const HasPlantInformation = HasPlantInformationUpgrade( @@ -1154,7 +1435,8 @@ function GetGardenDisplaySettings() { function CreatePlotElement( Plot, PlotIndex, - DisplaySettings + DisplaySettings, + IsPreview = false ) { const Button = document.createElement( @@ -1220,7 +1502,9 @@ function CreatePlotElement( ); Button.title = - "Empty plot"; + IsPreview + ? "Future Sight: empty plot" + : "Empty plot"; if ( DisplaySettings @@ -1235,9 +1519,13 @@ function CreatePlotElement( ); } + if (IsPreview) { + return Button; + } + if ( SelectedGardenTool === - "Seed" + "Trowel" ) { Button.addEventListener( "click", @@ -1247,6 +1535,21 @@ function CreatePlotElement( ); } ); + } else if ( + SelectedGardenTool === + "MagicTrowel" + ) { + Button.classList.add( + "GardenPlotMagicPlant" + ); + + Button.title = + "Fill empty plots with the selected seed"; + + Button.addEventListener( + "click", + PlantManySeeds + ); } return Button; @@ -1340,6 +1643,15 @@ function CreatePlotElement( } + if (IsPreview) { + Button.title = + "Future Sight: " + + Plant.Name; + + return Button; + } + + if ( SelectedGardenTool === "Shovel" @@ -1365,6 +1677,60 @@ function CreatePlotElement( } + if ( + SelectedGardenTool === + "Fertilizer" + ) { + if (Progress >= 1) { + Button.title = + Plant.Name + + " is already fully grown"; + + return Button; + } + + Button.classList.add( + "GardenPlotFertilize" + ); + + Button.title = + "Fertilize " + + Plant.Name + + " (" + + GetFertilizerGrowthMinutes( + GameSave + ) + + " minutes)"; + + Button.addEventListener( + "click", + () => { + FertilizePlant( + PlotIndex + ); + } + ); + + return Button; + } + + + if ( + SelectedGardenTool !== + "Trowel" + ) { + Button.title = + Plant.Name + + " - " + + Math.floor( + Progress * 100 + ) + + "% grown"; + + return Button; + } + + if ( Progress >= 1 ) { @@ -1615,6 +1981,129 @@ async function PlantSeed( } +async function PlantManySeeds() { + EnsureSelectedSeed(); + + if (SelectedSeedId === null) { + SetGardenMessage( + "You don't have any seeds to plant." + ); + + return; + } + + + const Plant = + GetPlantById( + SelectedSeedId + ); + + const PlantKey = + GetPlantKeyById( + SelectedSeedId + ); + + if ( + Plant === null || + PlantKey === null + ) { + return; + } + + + const EmptyPlotIndexes = []; + + GameSave.Garden.Plots.forEach( + (Plot, PlotIndex) => { + if (Plot === null) { + EmptyPlotIndexes.push( + PlotIndex + ); + } + } + ); + + if (EmptyPlotIndexes.length === 0) { + SetGardenMessage( + "There aren't any empty plots to fill." + ); + + return; + } + + + const SeedCount = + GetSeedCount( + GameSave, + SelectedSeedId + ); + + const PlantCount = Math.min( + SeedCount, + EmptyPlotIndexes.length + ); + + if (PlantCount <= 0) { + EnsureSelectedSeed(); + RenderGame(); + + SetGardenMessage( + "You don't have any of that seed left." + ); + + return; + } + + + if ( + !TakeSeed( + GameSave, + SelectedSeedId, + PlantCount + ) + ) { + return; + } + + + const PlantedAt = Date.now(); + + for ( + let Index = 0; + Index < PlantCount; + Index++ + ) { + GameSave.Garden.Plots[ + EmptyPlotIndexes[Index] + ] = { + Plant: PlantKey, + PlantedAt: PlantedAt + }; + } + + + SetGardenMessage( + "Magic trowel planted " + + PlantCount.toLocaleString() + + " " + + Plant.Name + + (PlantCount === 1 + ? " seed." + : " seeds.") + ); + + CheckGardenMutations( + GameSave + ); + + RenderGame(); + + await SaveGame( + GameSave + ); +} + + async function HarvestPlant( PlotIndex ) { @@ -1691,20 +2180,82 @@ async function HarvestPlant( async function RemovePlant( PlotIndex ) { - if ( + const Plot = GameSave.Garden.Plots[ PlotIndex - ] === null - ) { + ]; + + if (Plot === null) { return; } + const Plant = + Plants[Plot.Plant]; + + let ReturnMessage = ""; + + + if ( + Plant !== undefined && + DoesShovelReturnSeed( + GameSave + ) + ) { + AddSeed( + GameSave, + Plant.Id, + 1 + ); + + ReturnMessage = + " The seed was returned."; + } else if ( + Plant !== undefined + ) { + const RefundRate = + GetShovelRefundRate( + GameSave + ); + + const SeedCost = + GetPlantShopCost( + GameSave, + Plant.Id + ); + + const Refund = + SeedCost === null + ? 0 + : Math.floor( + SeedCost * + RefundRate + ); + + if (Refund > 0) { + GameSave.Currency.Dew += + Refund; + + ReturnMessage = + " Recovered " + + Refund.toLocaleString() + + " Dew."; + } + } + + GameSave.Garden.Plots[ PlotIndex ] = null; + SetGardenMessage( + "Removed " + + (Plant?.Name ?? "plant") + + "." + + ReturnMessage + ); + CheckGardenMutations( GameSave ); @@ -1717,14 +2268,205 @@ async function RemovePlant( } +async function FertilizePlant( + PlotIndex +) { + const Plot = + GameSave.Garden.Plots[ + PlotIndex + ]; + + if (Plot === null) { + return; + } + + + const Plant = + Plants[Plot.Plant]; + + if ( + Plant === undefined || + IsPlantMature( + Plot, + Plant + ) + ) { + return; + } + + + if ( + !TakeFertilizerUse( + GameSave + ) + ) { + SetGardenMessage( + "You're out of Fertilizer. Buy more in the Shop." + ); + + RenderTools(); + + return; + } + + + const Minutes = + GetFertilizerGrowthMinutes( + GameSave + ); + + Plot.PlantedAt -= + Minutes * 60 * 1000; + + + SetGardenMessage( + "Fertilized " + + Plant.Name + + ", advancing its growth by " + + Minutes + + (Minutes === 1 + ? " minute." + : " minutes.") + ); + + CheckGardenMutations( + GameSave + ); + + RenderGame(); + + await SaveGame( + GameSave + ); +} + + +async function UseFutureSight() { + if ( + !IsGardenToolUnlocked( + GameSave, + "FutureSight" + ) || + !IsFutureSightReady( + GameSave + ) + ) { + return; + } + + + const Preview = + CreateFutureSightPreview(); + + if (Preview === null) { + SelectedGardenTool = + "Trowel"; + + SetGardenMessage( + "Future Sight can't see a mutation from the current Garden." + ); + + RenderTools(); + RenderGarden(); + + return; + } + + + FutureSightPreview = Preview; + + StartFutureSightCooldown( + GameSave + ); + + + SetGardenMessage( + "Future Sight: highlighted plots show the next mutation succeeding." + ); + + RenderSeeds(); + RenderTools(); + RenderGarden(); + + await SaveGame( + GameSave + ); +} + + +function CreateFutureSightPreview() { + const PreviewSave = + NormalizeSaveData( + JSON.parse( + JSON.stringify( + GameSave + ) + ) + ); + + const Candidates = + FindGardenMutationCandidates( + PreviewSave + ) + .filter( + Candidate => + !IsMutationCandidateOnCooldown( + PreviewSave, + Candidate + ) + ) + .sort( + CompareMutationCandidates + ); + + if (Candidates.length === 0) { + return null; + } + + + const Candidate = + Candidates[0]; + + ApplyMutationResult( + PreviewSave, + Candidate, + Candidate.Success + ); + + + return { + MutationId: + Candidate.Mutation.Id, + + Plots: + PreviewSave.Garden.Plots.map( + Plot => + CloneMutationPlot( + Plot + ) + ) + }; +} + + async function GardenTick() { RenderGarden(); + RenderTools(); RenderNextHarvest(); RenderGardenOverview(); RenderGardenEconomy(); RenderGardenInfoLayout(); + if ( + SelectedGardenTool === + "FutureSight" && + FutureSightPreview !== null + ) { + return; + } + + if (MutationCheckPending) { return; } diff --git a/Scripts/Garden/Profile.js b/Scripts/Garden/Profile.js index c3090c1..f11e132 100644 --- a/Scripts/Garden/Profile.js +++ b/Scripts/Garden/Profile.js @@ -76,6 +76,14 @@ async function StartProfile() { function BindGardenDisplayPreferences() { const PreferenceButtons = [ + [ + "ToggleSelectQuickBoughtPlantButton", + "SelectQuickBoughtPlant" + ], + [ + "ToggleSelectTrowelWithInventoryPlantButton", + "SelectTrowelWithInventoryPlant" + ], [ "TogglePlantNamesButton", "ShowPlantNames" @@ -140,11 +148,49 @@ function BindGardenDisplayPreferences() { function RenderGardenDisplayPreferences() { + RenderToolSelectionPreferences(); RenderPlantInformationPreferences(); RenderGardenOverviewPreferences(); } +function RenderToolSelectionPreferences() { + const QuickBuyButton = + document.getElementById( + "ToggleSelectQuickBoughtPlantButton" + ); + + const TrowelButton = + document.getElementById( + "ToggleSelectTrowelWithInventoryPlantButton" + ); + + if ( + QuickBuyButton === null || + TrowelButton === null + ) { + return; + } + + + SetPreferenceButtonState( + QuickBuyButton, + "Select bought plant when you quick-buy one", + ProfileSave.Preferences + .SelectQuickBoughtPlant !== + false + ); + + SetPreferenceButtonState( + TrowelButton, + "Select trowel when you select an inventory plant", + ProfileSave.Preferences + .SelectTrowelWithInventoryPlant !== + false + ); +} + + function RenderPlantInformationPreferences() { const PlantNamesButton = document.getElementById( @@ -322,6 +368,11 @@ function SetPreferenceButtonState( async function ToggleGardenDisplayPreference( PreferenceName ) { + const ToolSelectionPreferences = [ + "SelectQuickBoughtPlant", + "SelectTrowelWithInventoryPlant" + ]; + const PlantInformationPreferences = [ "ShowPlantNames", "ShowGrowthTimers" @@ -337,6 +388,11 @@ async function ToggleGardenDisplayPreference( ]; + const IsToolSelectionPreference = + ToolSelectionPreferences.includes( + PreferenceName + ); + const IsPlantInformationPreference = PlantInformationPreferences.includes( PreferenceName @@ -349,6 +405,7 @@ async function ToggleGardenDisplayPreference( if ( + !IsToolSelectionPreference && !IsPlantInformationPreference && !IsGardenOverviewPreference ) { diff --git a/Scripts/Garden/Save.js b/Scripts/Garden/Save.js index 30ab06f..9d80ca6 100644 --- a/Scripts/Garden/Save.js +++ b/Scripts/Garden/Save.js @@ -392,7 +392,7 @@ function CreateNewSave() { return NormalizeSaveData({ - Version: 3, + Version: 4, Revision: 0, LastSavedAt: Date.now(), @@ -413,7 +413,18 @@ function CreateNewSave() { }, Inventory: { - Seeds: SeedInventory + Seeds: SeedInventory, + Fertilizer: 0 + }, + + Tools: { + MagicTrowel: false, + Shovel: false, + Fertilizer: false, + FutureSight: false, + ShovelLevel: 0, + FertilizerLevel: 0, + FutureSightCooldownUntil: 0 }, Upgrades: { @@ -432,7 +443,9 @@ function CreateNewSave() { ShowEmptyPlots: true, ShowPlantedPlots: true, ShowGrowingPlots: true, - ShowReadyPlots: true + ShowReadyPlots: true, + SelectQuickBoughtPlant: true, + SelectTrowelWithInventoryPlant: true }, Gardens: [ @@ -456,7 +469,7 @@ function NormalizeSaveData( Number( SaveData.Version ?? 1 ), - 3 + 4 ); SaveData.Revision = Number( @@ -492,6 +505,14 @@ function NormalizeSaveData( SaveData.Inventory ??= {}; SaveData.Inventory.Seeds ??= {}; + SaveData.Inventory.Fertilizer = Math.max( + 0, + Math.floor( + Number( + SaveData.Inventory.Fertilizer ?? 0 + ) || 0 + ) + ); /* @@ -563,6 +584,54 @@ function NormalizeSaveData( delete SaveData.Seeds; + SaveData.Tools ??= {}; + + SaveData.Tools.MagicTrowel = + SaveData.Tools.MagicTrowel === true; + + SaveData.Tools.Shovel = + SaveData.Tools.Shovel === true; + + SaveData.Tools.Fertilizer = + SaveData.Tools.Fertilizer === true; + + SaveData.Tools.FutureSight = + SaveData.Tools.FutureSight === true; + + SaveData.Tools.ShovelLevel = Math.max( + 0, + Math.min( + 3, + Math.floor( + Number( + SaveData.Tools.ShovelLevel ?? 0 + ) || 0 + ) + ) + ); + + SaveData.Tools.FertilizerLevel = Math.max( + 0, + Math.min( + 5, + Math.floor( + Number( + SaveData.Tools.FertilizerLevel ?? 0 + ) || 0 + ) + ) + ); + + SaveData.Tools.FutureSightCooldownUntil = Math.max( + 0, + Math.floor( + Number( + SaveData.Tools.FutureSightCooldownUntil ?? 0 + ) || 0 + ) + ); + + SaveData.Discoveries ??= {}; SaveData.Discoveries.Plants = @@ -654,6 +723,14 @@ function NormalizeSaveData( SaveData.Preferences.ShowReadyPlots !== false; + SaveData.Preferences.SelectQuickBoughtPlant = + SaveData.Preferences.SelectQuickBoughtPlant !== + false; + + SaveData.Preferences.SelectTrowelWithInventoryPlant = + SaveData.Preferences.SelectTrowelWithInventoryPlant !== + false; + const LegacyGarden = SaveData.Garden ?? diff --git a/Scripts/Garden/Shop.js b/Scripts/Garden/Shop.js index eaa2baa..0ce1c70 100644 --- a/Scripts/Garden/Shop.js +++ b/Scripts/Garden/Shop.js @@ -30,6 +30,11 @@ async function StartShop() { function BindShopSectionToggles() { + BindShopSectionToggle( + "ToolsToggle", + "ToolsContent" + ); + BindShopSectionToggle( "PermanentUpgradesToggle", "PermanentUpgradesContent" @@ -134,6 +139,7 @@ function BindShopSeedControls() { function RenderShop() { RenderShopCurrency(); + RenderToolShop(); RenderShopSeeds(); RenderGardenUpgrades(); } @@ -422,6 +428,692 @@ function CompareShopNumbers( } +function RenderToolShop() { + const ToolList = + document.getElementById( + "ShopToolList" + ); + + if (ToolList === null) { + return; + } + + + ToolList.replaceChildren( + CreateTrowelToolCard(), + CreateToolUnlockCard( + "MagicTrowel", + "Magic trowel", + "Plant the selected seed into as many empty plots as possible in one use.", + [ + [ + "Planting", + "All empty plots" + ] + ] + ), + CreateShovelToolCard(), + CreateFertilizerToolCard(), + CreateToolUnlockCard( + "FutureSight", + "Future Sight", + "Preview what the Garden would look like if its next eligible mutation succeeded. Future Sight has a two-hour cooldown.", + [ + [ + "Cooldown", + "2 hours" + ] + ] + ) + ); +} + + +function CreateTrowelToolCard() { + const Card = + document.createElement( + "article" + ); + + Card.className = + "Panel ShopItem"; + + + const Header = + document.createElement( + "div" + ); + + Header.className = + "ShopItemHeader ShopItemHeaderOwned"; + + + const Name = + document.createElement( + "h3" + ); + + Name.textContent = "Trowel"; + + Header.appendChild(Name); + + + const Description = + document.createElement( + "p" + ); + + Description.textContent = + "Your default planting tool. Select a seed, then use the Trowel on an empty plot."; + + + const Details = + document.createElement( + "div" + ); + + Details.className = + "ShopItemDetails"; + + Details.append( + CreateShopStat( + "Status", + "Unlocked" + ), + CreateShopStat( + "Cost", + "Free" + ) + ); + + + const Button = + document.createElement( + "button" + ); + + Button.className = + "ActionButton ShopBuyButton"; + + Button.type = "button"; + Button.textContent = "Default tool"; + Button.disabled = true; + + + Card.append( + Header, + Description, + Details, + Button + ); + + return Card; +} + + +function CreateToolUnlockCard( + Tool, + NameText, + DescriptionText, + ExtraStats = [] +) { + const IsOwned = + IsGardenToolUnlocked( + ShopSave, + Tool + ); + + const Cost = + GetGardenToolUnlockCost( + Tool + ); + + + const Card = + document.createElement( + "article" + ); + + Card.className = + "Panel ShopItem"; + + + const Header = + document.createElement( + "div" + ); + + Header.className = + "ShopItemHeader" + + ( + IsOwned + ? " ShopItemHeaderOwned" + : "" + ); + + + const Name = + document.createElement( + "h3" + ); + + Name.textContent = NameText; + + Header.appendChild(Name); + + + const Description = + document.createElement( + "p" + ); + + Description.textContent = + DescriptionText; + + + const Details = + document.createElement( + "div" + ); + + Details.className = + "ShopItemDetails"; + + Details.appendChild( + CreateShopStat( + "Status", + IsOwned + ? "Unlocked" + : "Locked" + ) + ); + + for ( + const [Label, Value] + of ExtraStats + ) { + Details.appendChild( + CreateShopStat( + Label, + Value + ) + ); + } + + + 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", + () => { + BuyGardenTool( + Tool, + NameText + ); + } + ); + } + + + Card.append( + Header, + Description, + Details, + BuyButton + ); + + return Card; +} + + +function CreateShovelToolCard() { + const IsOwned = + IsGardenToolUnlocked( + ShopSave, + "Shovel" + ); + + const Level = + GetShovelLevel( + ShopSave + ); + + const UpgradeCost = + IsOwned + ? GetShovelUpgradeCost( + ShopSave + ) + : null; + + + const Card = + document.createElement( + "article" + ); + + Card.className = + "Panel ShopItem"; + + + const Header = + document.createElement( + "div" + ); + + Header.className = + "ShopItemHeader" + + ( + IsOwned + ? " ShopItemHeaderOwned" + : "" + ); + + + const Name = + document.createElement( + "h3" + ); + + Name.textContent = "Shovel"; + + Header.appendChild(Name); + + + const Description = + document.createElement( + "p" + ); + + Description.textContent = + "Remove planted crops. Upgrades recover more of the seed value instead of wasting it."; + + + const Details = + document.createElement( + "div" + ); + + Details.className = + "ShopItemDetails"; + + Details.append( + CreateShopStat( + "Status", + IsOwned + ? "Unlocked" + : "Locked" + ), + CreateShopStat( + IsOwned + ? "Current" + : "On unlock", + GetShovelLevelDescription( + Level + ) + ), + CreateShopStat( + IsOwned + ? "Next" + : "First upgrade", + Level >= ShovelMaximumLevel + ? "Maximum level" + : GetShovelLevelDescription( + Level + 1 + ) + ) + ); + + + const ActionButton = + document.createElement( + "button" + ); + + ActionButton.className = + "ActionButton ShopBuyButton"; + + ActionButton.type = "button"; + + if (!IsOwned) { + const UnlockCost = + GetGardenToolUnlockCost( + "Shovel" + ); + + ActionButton.textContent = + "Buy - " + + UnlockCost.toLocaleString() + + " Dew"; + + ActionButton.disabled = + ShopSave.Currency.Dew < + UnlockCost || + ShopPurchasePending; + + ActionButton.addEventListener( + "click", + () => { + BuyGardenTool( + "Shovel", + "Shovel" + ); + } + ); + } else if (UpgradeCost === null) { + ActionButton.textContent = + "Maximum level"; + + ActionButton.disabled = true; + } else { + ActionButton.textContent = + "Upgrade - " + + UpgradeCost.toLocaleString() + + " Dew"; + + ActionButton.disabled = + ShopSave.Currency.Dew < + UpgradeCost || + ShopPurchasePending; + + ActionButton.addEventListener( + "click", + BuyShovelUpgrade + ); + } + + + Card.append( + Header, + Description, + Details, + ActionButton + ); + + return Card; +} + + +function GetShovelLevelDescription( + Level +) { + if (Level <= 0) { + return "Level 0 - returns nothing"; + } + + if (Level === 1) { + return "Level 1 - returns 33% of seed cost"; + } + + if (Level === 2) { + return "Level 2 - returns 66% of seed cost"; + } + + return "Level 3 - returns the planted seed"; +} + + +function CreateFertilizerToolCard() { + const IsOwned = + IsGardenToolUnlocked( + ShopSave, + "Fertilizer" + ); + + const Level = + GetFertilizerLevel( + ShopSave + ); + + const UpgradeCost = + IsOwned + ? GetFertilizerUpgradeCost( + ShopSave + ) + : null; + + + const Card = + document.createElement( + "article" + ); + + Card.className = + "Panel ShopItem"; + + + const Header = + document.createElement( + "div" + ); + + Header.className = + "ShopItemHeader" + + ( + IsOwned + ? " ShopItemHeaderOwned" + : "" + ); + + + const Name = + document.createElement( + "h3" + ); + + Name.textContent = "Fertilizer"; + + Header.appendChild(Name); + + + const Description = + document.createElement( + "p" + ); + + Description.textContent = + "Spend Fertilizer uses to advance a growing plant. Stronger levels advance more growth per use."; + + + const Details = + document.createElement( + "div" + ); + + Details.className = + "ShopItemDetails"; + + Details.append( + CreateShopStat( + "Status", + IsOwned + ? "Unlocked" + : "Locked" + ), + CreateShopStat( + "Current uses", + Number( + ShopSave.Inventory + .Fertilizer ?? 0 + ).toLocaleString() + ), + CreateShopStat( + IsOwned + ? "Current" + : "On unlock", + "Level " + + Level + + " - " + + FertilizerGrowthMinutes[ + Level + ] + + " minutes" + ), + CreateShopStat( + IsOwned + ? "Next" + : "First upgrade", + Level >= FertilizerMaximumLevel + ? "Maximum level" + : "Level " + + (Level + 1) + + " - " + + FertilizerGrowthMinutes[ + Level + 1 + ] + + " minutes" + ) + ); + + + const Actions = + document.createElement( + "div" + ); + + Actions.className = + "ShopItemActions"; + + + const ActionButton = + document.createElement( + "button" + ); + + ActionButton.className = + "ActionButton ShopBuyButton"; + + ActionButton.type = "button"; + + if (!IsOwned) { + const UnlockCost = + GetGardenToolUnlockCost( + "Fertilizer" + ); + + ActionButton.textContent = + "Buy - " + + UnlockCost.toLocaleString() + + " Dew"; + + ActionButton.disabled = + ShopSave.Currency.Dew < + UnlockCost || + ShopPurchasePending; + + ActionButton.addEventListener( + "click", + () => { + BuyGardenTool( + "Fertilizer", + "Fertilizer" + ); + } + ); + } else if (UpgradeCost === null) { + ActionButton.textContent = + "Maximum level"; + + ActionButton.disabled = true; + } else { + ActionButton.textContent = + "Upgrade - " + + UpgradeCost.toLocaleString() + + " Dew"; + + ActionButton.disabled = + ShopSave.Currency.Dew < + UpgradeCost || + ShopPurchasePending; + + ActionButton.addEventListener( + "click", + BuyFertilizerUpgrade + ); + } + + Actions.appendChild( + ActionButton + ); + + + if (IsOwned) { + for (const Amount of [1, 10]) { + const Cost = + GetFertilizerUseCost( + Amount + ); + + const Button = + document.createElement( + "button" + ); + + Button.className = + "ActionButton ShopBuyButton"; + + Button.type = "button"; + + Button.textContent = + "Buy uses ×" + + Amount + + " - " + + Cost.toLocaleString() + + " Dew"; + + Button.disabled = + ShopSave.Currency.Dew < Cost || + ShopPurchasePending; + + Button.addEventListener( + "click", + () => { + BuyFertilizerUses( + Amount + ); + } + ); + + Actions.appendChild( + Button + ); + } + } + + + Card.append( + Header, + Description, + Details, + Actions + ); + + return Card; +} + + function RenderGardenUpgrades() { const UpgradeList = document.getElementById( @@ -1727,6 +2419,296 @@ async function BuyPlantSeed( } } +async function BuyGardenTool( + Tool, + Name +) { + if ( + ShopPurchasePending || + IsGardenToolUnlocked( + ShopSave, + Tool + ) + ) { + return; + } + + + const Cost = + GetGardenToolUnlockCost( + Tool + ); + + if (Cost === null) { + return; + } + + if ( + ShopSave.Currency.Dew < + Cost + ) { + SetShopMessage( + "You don't have enough Dew for " + + Name + + "." + ); + + return; + } + + + ShopPurchasePending = true; + + try { + ShopSave.Currency.Dew -= + Cost; + + ShopSave.Statistics.CurrencySpent.Dew += + Cost; + + UnlockGardenTool( + ShopSave, + Tool + ); + + + SetShopMessage( + "Unlocked " + + Name + + " for " + + Cost.toLocaleString() + + " Dew." + ); + + RenderShop(); + + await SaveGame( + ShopSave + ); + } finally { + ShopPurchasePending = false; + RenderShop(); + } +} + + +async function BuyShovelUpgrade() { + if ( + ShopPurchasePending || + !IsGardenToolUnlocked( + ShopSave, + "Shovel" + ) + ) { + return; + } + + + const Cost = + GetShovelUpgradeCost( + ShopSave + ); + + if (Cost === null) { + return; + } + + if ( + ShopSave.Currency.Dew < + Cost + ) { + SetShopMessage( + "You don't have enough Dew for the next Shovel upgrade." + ); + + return; + } + + + ShopPurchasePending = true; + + try { + ShopSave.Currency.Dew -= + Cost; + + ShopSave.Statistics.CurrencySpent.Dew += + Cost; + + if (!UpgradeShovel(ShopSave)) { + return; + } + + + SetShopMessage( + "Upgraded Shovel to level " + + GetShovelLevel( + ShopSave + ) + + " for " + + Cost.toLocaleString() + + " Dew." + ); + + RenderShop(); + + await SaveGame( + ShopSave + ); + } finally { + ShopPurchasePending = false; + RenderShop(); + } +} + + +async function BuyFertilizerUpgrade() { + if ( + ShopPurchasePending || + !IsGardenToolUnlocked( + ShopSave, + "Fertilizer" + ) + ) { + return; + } + + + const Cost = + GetFertilizerUpgradeCost( + ShopSave + ); + + if (Cost === null) { + return; + } + + if ( + ShopSave.Currency.Dew < + Cost + ) { + SetShopMessage( + "You don't have enough Dew for the next Fertilizer upgrade." + ); + + return; + } + + + ShopPurchasePending = true; + + try { + ShopSave.Currency.Dew -= + Cost; + + ShopSave.Statistics.CurrencySpent.Dew += + Cost; + + if (!UpgradeFertilizer(ShopSave)) { + return; + } + + + SetShopMessage( + "Upgraded Fertilizer to level " + + GetFertilizerLevel( + ShopSave + ) + + " for " + + Cost.toLocaleString() + + " Dew. Each use now advances " + + GetFertilizerGrowthMinutes( + ShopSave + ) + + " minutes." + ); + + RenderShop(); + + await SaveGame( + ShopSave + ); + } finally { + ShopPurchasePending = false; + RenderShop(); + } +} + + +async function BuyFertilizerUses( + Amount +) { + if ( + ShopPurchasePending || + !IsGardenToolUnlocked( + ShopSave, + "Fertilizer" + ) + ) { + return; + } + + + Amount = Math.max( + 1, + Math.floor( + Number(Amount) || 1 + ) + ); + + const Cost = + GetFertilizerUseCost( + Amount + ); + + if ( + ShopSave.Currency.Dew < + Cost + ) { + SetShopMessage( + "You don't have enough Dew for that much Fertilizer." + ); + + return; + } + + + ShopPurchasePending = true; + + try { + ShopSave.Currency.Dew -= + Cost; + + ShopSave.Statistics.CurrencySpent.Dew += + Cost; + + AddFertilizerUses( + ShopSave, + Amount + ); + + + SetShopMessage( + "Bought " + + Amount.toLocaleString() + + (Amount === 1 + ? " Fertilizer use for " + : " Fertilizer uses for ") + + Cost.toLocaleString() + + " Dew." + ); + + RenderShop(); + + await SaveGame( + ShopSave + ); + } finally { + ShopPurchasePending = false; + RenderShop(); + } +} + + async function BuyGardenExpansion( Direction ) { diff --git a/Scripts/Garden/Upgrades.js b/Scripts/Garden/Upgrades.js index 5e0ff95..e831ca7 100644 --- a/Scripts/Garden/Upgrades.js +++ b/Scripts/Garden/Upgrades.js @@ -1,3 +1,24 @@ +const MagicTrowelUnlockCost = 1000; +const ShovelUnlockCost = 50; +const FertilizerUnlockCost = 200; +const FutureSightUnlockCost = 5000; + +const ToolUpgradeCostMultiplier = 2; +const ShovelMaximumLevel = 3; +const FertilizerMaximumLevel = 5; +const FertilizerUseCost = 100; +const FertilizerGrowthMinutes = [ + 10, + 20, + 30, + 40, + 50, + 60 +]; + +const FutureSightCooldown = + 2 * 60 * 60 * 1000; + const GardenExpansionBaseCost = 1000; const PlantInformationUpgradeCost = 500; const GardenOverviewUpgradeCost = 1000; @@ -281,3 +302,324 @@ function UnlockMutationHints( SaveData.Upgrades.MutationHints = true; } + + +function IsGardenToolUnlocked( + SaveData, + Tool +) { + if (Tool === "Trowel") { + return true; + } + + if (Tool === "MagicTrowel") { + return SaveData?.Tools?.MagicTrowel === true; + } + + if (Tool === "Shovel") { + return SaveData?.Tools?.Shovel === true; + } + + if (Tool === "Fertilizer") { + return SaveData?.Tools?.Fertilizer === true; + } + + if (Tool === "FutureSight") { + return SaveData?.Tools?.FutureSight === true; + } + + return false; +} + + +function GetGardenToolUnlockCost( + Tool +) { + const Costs = { + MagicTrowel: MagicTrowelUnlockCost, + Shovel: ShovelUnlockCost, + Fertilizer: FertilizerUnlockCost, + FutureSight: FutureSightUnlockCost + }; + + return Costs[Tool] ?? null; +} + + +function UnlockGardenTool( + SaveData, + Tool +) { + SaveData.Tools ??= {}; + + if ( + ![ + "MagicTrowel", + "Shovel", + "Fertilizer", + "FutureSight" + ].includes(Tool) + ) { + return false; + } + + SaveData.Tools[Tool] = true; + + return true; +} + + +function GetShovelLevel( + SaveData +) { + return Math.max( + 0, + Math.min( + ShovelMaximumLevel, + Math.floor( + Number( + SaveData?.Tools?.ShovelLevel ?? 0 + ) || 0 + ) + ) + ); +} + + +function GetShovelUpgradeCost( + SaveData +) { + const Level = GetShovelLevel( + SaveData + ); + + if (Level >= ShovelMaximumLevel) { + return null; + } + + return Math.round( + ShovelUnlockCost * + Math.pow( + ToolUpgradeCostMultiplier, + Level + 1 + ) + ); +} + + +function UpgradeShovel( + SaveData +) { + if ( + !IsGardenToolUnlocked( + SaveData, + "Shovel" + ) + ) { + return false; + } + + const Level = GetShovelLevel( + SaveData + ); + + if (Level >= ShovelMaximumLevel) { + return false; + } + + SaveData.Tools.ShovelLevel = + Level + 1; + + return true; +} + + +function GetShovelRefundRate( + SaveData +) { + const Level = GetShovelLevel( + SaveData + ); + + if (Level === 1) { + return 0.33; + } + + if (Level === 2) { + return 0.66; + } + + return 0; +} + + +function DoesShovelReturnSeed( + SaveData +) { + return GetShovelLevel( + SaveData + ) >= 3; +} + + +function GetFertilizerLevel( + SaveData +) { + return Math.max( + 0, + Math.min( + FertilizerMaximumLevel, + Math.floor( + Number( + SaveData?.Tools?.FertilizerLevel ?? 0 + ) || 0 + ) + ) + ); +} + + +function GetFertilizerUpgradeCost( + SaveData +) { + const Level = GetFertilizerLevel( + SaveData + ); + + if (Level >= FertilizerMaximumLevel) { + return null; + } + + return Math.round( + FertilizerUnlockCost * + Math.pow( + ToolUpgradeCostMultiplier, + Level + 1 + ) + ); +} + + +function UpgradeFertilizer( + SaveData +) { + if ( + !IsGardenToolUnlocked( + SaveData, + "Fertilizer" + ) + ) { + return false; + } + + const Level = GetFertilizerLevel( + SaveData + ); + + if (Level >= FertilizerMaximumLevel) { + return false; + } + + SaveData.Tools.FertilizerLevel = + Level + 1; + + return true; +} + + +function GetFertilizerGrowthMinutes( + SaveData +) { + return FertilizerGrowthMinutes[ + GetFertilizerLevel( + SaveData + ) + ]; +} + + +function GetFertilizerUseCost( + Amount = 1 +) { + return FertilizerUseCost * + Math.max( + 1, + Math.floor( + Number(Amount) || 1 + ) + ); +} + + +function AddFertilizerUses( + SaveData, + Amount = 1 +) { + SaveData.Inventory ??= {}; + SaveData.Inventory.Fertilizer ??= 0; + + SaveData.Inventory.Fertilizer += + Math.max( + 0, + Math.floor( + Number(Amount) || 0 + ) + ); +} + + +function TakeFertilizerUse( + SaveData +) { + const CurrentUses = Math.max( + 0, + Math.floor( + Number( + SaveData?.Inventory?.Fertilizer ?? 0 + ) || 0 + ) + ); + + if (CurrentUses <= 0) { + return false; + } + + SaveData.Inventory.Fertilizer = + CurrentUses - 1; + + return true; +} + + +function GetFutureSightCooldownRemaining( + SaveData, + Now = Date.now() +) { + return Math.max( + 0, + Number( + SaveData?.Tools?.FutureSightCooldownUntil ?? 0 + ) - Now + ); +} + + +function IsFutureSightReady( + SaveData, + Now = Date.now() +) { + return GetFutureSightCooldownRemaining( + SaveData, + Now + ) <= 0; +} + + +function StartFutureSightCooldown( + SaveData, + Now = Date.now() +) { + SaveData.Tools ??= {}; + SaveData.Tools.FutureSightCooldownUntil = + Now + FutureSightCooldown; +} diff --git a/Styles/Style.css b/Styles/Style.css index eff0b72..55e4ec5 100644 --- a/Styles/Style.css +++ b/Styles/Style.css @@ -719,6 +719,20 @@ input, button, textarea, select { gap: 0.5rem; } +.ToolButtonTrowel[aria-current="true"] { + --ActionAccent: var(--Green); + color: var(--Green); + background-color: var(--Surface1); + border-color: var(--Green); +} + +.ToolButtonMagicTrowel[aria-current="true"] { + --ActionAccent: var(--Mauve); + color: var(--Mauve); + background-color: var(--Surface1); + border-color: var(--Mauve); +} + .ToolButtonShovel[aria-current="true"] { --ActionAccent: var(--Red); color: var(--Red); @@ -726,11 +740,62 @@ input, button, textarea, select { border-color: var(--Red); } +.ToolButtonFertilizer[aria-current="true"] { + --ActionAccent: var(--Yellow); + color: var(--Yellow); + background-color: var(--Surface1); + border-color: var(--Yellow); +} + +.ToolButtonFutureSight[aria-current="true"] { + --ActionAccent: var(--Lavender); + color: var(--Lavender); + background-color: var(--Surface1); + border-color: var(--Lavender); +} + +.ToolButton:disabled { + cursor: not-allowed; + color: var(--Overlay1); + background-color: var(--Mantle); + border-color: var(--Surface1); + box-shadow: none; +} + +.GardenPlotMagicPlant:hover { + --PlotBorderColour: var(--Mauve); +} + .GardenPlotRemove:hover { --GrowthColour: var(--Red); --PlotBorderColour: var(--Red); } +.GardenPlotFertilize:hover { + --GrowthColour: var(--Yellow); + --PlotBorderColour: var(--Yellow); +} + +.GardenPlotsFutureSight { + border-color: var(--Lavender); +} + +.GardenPlotFutureSight { + cursor: default; +} + +.GardenPlotFutureSight:hover { + transform: none; + box-shadow: 3px 3px 0 var(--Crust); +} + +.GardenPlotFutureSightChanged { + --GrowthColour: var(--Lavender); + --PlotBorderColour: var(--Lavender); + outline: 3px solid var(--Lavender); + outline-offset: -3px; +} + .GardenUser, .Leaderboard { margin-top: 1.5rem; padding: 1rem; @@ -1344,6 +1409,7 @@ input, button, textarea, select { padding: 0.65rem 0.8rem; } +.ShopItemActions, .ShopSeedBuyOptions { display: flex; flex-direction: column;