let GameSave; let SelectedSeedId = null; let SelectedGardenTool = "Trowel"; let FutureSightPreview = null; let MutationCheckPending = false; let QuickBuyPurchasePending = false; let QuickBuyAmountMode = "One"; const GardenUpdateInterval = 1000; async function StartGame() { try { await LoadGameContent(); } catch (Error) { console.error( "Couldn't load Garden content:", Error ); SetGardenMessage( "Couldn't load plant or mutation data." ); return; } GameSave = await LoadGame(); const InitialSimulationResult = AdvanceGameSimulation( GameSave, Date.now() ); BindGardenTools(); BindGardenSelector(); BindQuickBuyControls(); EnsureSelectedSeed(); RenderGame(); if ( InitialSimulationResult.Advanced || InitialSimulationResult.Dirty ) { await SaveGame( GameSave ); } setInterval( GardenTick, GardenUpdateInterval ); await RenderUser(); } function RenderGame() { EnsureSelectedGardenTool(); EnsureSelectedSeed(); RenderCurrency(); RenderQuickBuy(); RenderSeeds(); RenderTools(); RenderGardenSelector(); RenderGarden(); RenderNextHarvest(); RenderGardenOverview(); RenderGardenEconomy(); RenderGardenInfoLayout(); } function BindGardenSelector() { const PreviousButton = document.getElementById( "PreviousGardenButton" ); const NextButton = document.getElementById( "NextGardenButton" ); const NameInput = document.getElementById( "GardenNameInput" ); PreviousButton?.addEventListener( "click", () => { 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 ActionTime = Date.now(); AdvanceGameSimulation( GameSave, ActionTime ); const GardenCount = GameSave.Gardens.length; if (GardenCount <= 1) { return; } const NewIndex = GameSave.ActiveGardenIndex + Direction; if ( NewIndex < 0 || NewIndex >= GardenCount ) { return; } SetActiveGardenIndex( GameSave, NewIndex ); FutureSightPreview = null; if ( SelectedGardenTool === "FutureSight" ) { SelectedGardenTool = "Trowel"; } const MutationResult = CheckGardenMutations( GameSave, ActionTime ); RenderGame(); SetGardenMessage( "Switched to " + GameSave.Garden.Name + "." ); await SaveGame( GameSave ); if (MutationResult.Attempted) { RenderGame(); } } function RenderGardenSelector() { RenderGardenSelectorView({ Gardens: GameSave.Gardens, GardenIndex: GameSave.ActiveGardenIndex, PreviousButton: document.getElementById( "PreviousGardenButton" ), NameInput: document.getElementById( "GardenNameInput" ), NextButton: document.getElementById( "NextGardenButton" ), PreserveFocusedName: true }); } function CheckAllGardenMutations( SaveData, AtTime = GetSimulationTime( SaveData ) ) { return CheckAllGardenMutationsAtTime( SaveData, AtTime ); } function BindGardenTools() { const ToolList = document.getElementById( "ToolList" ); if (ToolList === null) { return; } ToolList.addEventListener( "click", Event => { const Button = Event.target.closest( "[data-garden-tool]" ); if ( Button === null || !ToolList.contains( Button ) ) { return; } const Tool = Button.dataset .gardenTool; SelectGardenTool( Tool ); } ); } 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 ( !IsGardenToolUnlocked( GameSave, SelectedGardenTool ) ) { SelectedGardenTool = "Trowel"; FutureSightPreview = null; } } function RenderTools() { const ToolList = document.getElementById( "ToolList" ); 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 ); } } function BindQuickBuyControls() { const Controls = [ [ "QuickBuyOneButton", "One" ], [ "QuickBuyTenButton", "Ten" ], [ "QuickBuyGardenButton", "Garden" ] ]; for ( const [ButtonId, Mode] of Controls ) { const Button = document.getElementById( ButtonId ); Button?.addEventListener( "click", () => { QuickBuyAmountMode = Mode; RenderQuickBuy(); } ); } } function GetQuickBuyAmount() { if ( QuickBuyAmountMode === "Ten" ) { return 10; } 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 TenButton = document.getElementById( "QuickBuyTenButton" ); const GardenButton = document.getElementById( "QuickBuyGardenButton" ); const AmountButtons = [ [OneButton, "One", 1], [TenButton, "Ten", 10], [ 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 ); if ( GameSave.Preferences .SelectQuickBoughtPlant !== false ) { SelectInventoryPlant( 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( "DewAmount" ); DewAmount.textContent = GameSave.Currency.Dew .toLocaleString(); } function EnsureSelectedSeed() { if ( SelectedSeedId !== null && GetSeedCount( GameSave, SelectedSeedId ) > 0 ) { return; } SelectedSeedId = null; const SeedIds = Object.keys( GameSave.Inventory.Seeds ) .map(Number) .filter( PlantId => GetSeedCount( GameSave, PlantId ) > 0 && GetPlantById( PlantId ) !== null ) .sort( (A, B) => A - B ); if (SeedIds.length > 0) { SelectedSeedId = SeedIds[0]; return; } } function SelectInventoryPlant( PlantId ) { if ( GetPlantById( PlantId ) === null || GetSeedCount( GameSave, PlantId ) <= 0 ) { return false; } SelectedSeedId = Number(PlantId); if ( GameSave.Preferences .SelectTrowelWithInventoryPlant !== false ) { SelectedGardenTool = "Trowel"; FutureSightPreview = null; } return true; } function RenderSeeds() { const SeedList = document.getElementById( "SeedList" ); SeedList.replaceChildren(); const SeedIds = Object.keys( GameSave.Inventory.Seeds ) .map(Number) .filter( PlantId => GetSeedCount( GameSave, PlantId ) > 0 && GetPlantById( PlantId ) !== null ) .sort( (A, B) => A - B ); if (SeedIds.length === 0) { const EmptyMessage = document.createElement( "p" ); EmptyMessage.className = "SeedEmpty"; EmptyMessage.append( "You don't have any seeds. Visit the " ); const ShopLink = document.createElement( "a" ); ShopLink.href = "/Pages/Shop.html"; ShopLink.textContent = "shop"; EmptyMessage.append( ShopLink, "." ); SeedList.appendChild( EmptyMessage ); return; } for ( const PlantId of SeedIds ) { const Plant = GetPlantById( PlantId ); if (Plant === null) { continue; } const Button = document.createElement( "button" ); Button.className = "ActionButton SeedButton"; Button.type = "button"; Button.textContent = Plant.Name + " ×" + GetSeedCount( GameSave, PlantId ); if ( PlantId === SelectedSeedId ) { Button.setAttribute( "aria-current", "true" ); } Button.addEventListener( "click", () => { SelectInventoryPlant( PlantId ); RenderSeeds(); RenderTools(); RenderGarden(); SetGardenMessage( "Selected " + Plant.Name + "." ); } ); SeedList.appendChild( Button ); } } function RenderGarden() { const GardenGrid = document.getElementById( "GardenGrid" ); const GardenPanel = document.getElementById( "Plots" ); GardenGrid.style.setProperty( "--GardenWidth", GameSave.Garden.Width ); GardenPanel.style.setProperty( "--GardenBorderColour", GetGardenBorderColour( GameSave.ActiveGardenIndex ) ); 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( DisplayPlot, PlotIndex, DisplaySettings, IsFutureSightPreview ); if (IsFutureSightPreview) { PlotElement.classList.add( "GardenPlotFutureSight" ); if ( !AreGardenPlotsEqual( Plot, DisplayPlot ) ) { PlotElement.classList.add( "GardenPlotFutureSightChanged" ); } } GardenGrid.appendChild( PlotElement ); } ); } function AreGardenPlotsEqual( A, B ) { if (A === null || B === null) { return A === B; } return ( A.Plant === B.Plant && Number(A.PlantedAt) === Number(B.PlantedAt) && GetGardenPlotRotation(A) === GetGardenPlotRotation(B) ); } function GetGardenDisplaySettings() { const HasPlantInformation = HasPlantInformationUpgrade( GameSave ); return { ShowPlantNames: HasPlantInformation && GameSave.Preferences .ShowPlantNames !== false, ShowGrowthTimers: HasPlantInformation && GameSave.Preferences .ShowGrowthTimers !== false, ShowPlotRotation: HasRotationUpgrade( GameSave ) && GameSave.Preferences .ShowPlotRotation !== false }; } async function RotateGardenPlot( PlotIndex ) { if ( !HasRotationUpgrade( GameSave ) ) { return; } const ActionTime = Date.now(); AdvanceGameSimulation( GameSave, ActionTime ); const Plot = GameSave.Garden.Plots[ PlotIndex ]; if ( Plot === null || typeof Plot !== "object" ) { return; } const CurrentIndex = GardenPlotRotations.indexOf( GetGardenPlotRotation( Plot ) ); Plot.Rotation = GardenPlotRotations[ (CurrentIndex + 1) % GardenPlotRotations.length ]; const Plant = Plants[Plot.Plant]; SetGardenMessage( "Rotated " + (Plant?.Name ?? "plot") + " " + Plot.Rotation.toLowerCase() + "." ); RenderGame(); await SaveGame( GameSave ); } function CreatePlotElement( Plot, PlotIndex, DisplaySettings, IsPreview = false ) { const Plant = Plot === null ? null : Plants[Plot.Plant]; let Progress = 0; let CooldownState = null; let BorderProgress = 0; let ImagePath = null; let TimerText = ""; if ( Plot !== null && Plant !== undefined ) { Progress = GetPlantGrowthProgress( Plot, Plant ); CooldownState = Progress >= 1 && typeof GetFunctionalPlantCooldownDisplayState === "function" ? GetFunctionalPlantCooldownDisplayState( Plot, Plant, GetSimulationTime( GameSave ) ) : null; BorderProgress = CooldownState === null ? Progress : CooldownState.Progress; ImagePath = GetPlantImage( Plot, Plant ); TimerText = GetGardenPlotTimerText( Plot, Plant, Progress, GetSimulationTime( GameSave ), CooldownState ); } const View = CreateGardenPlotView({ Plot, Plant, Progress, BorderProgress, DisplaySettings, ImagePath, TimerText, FunctionalCooldown: CooldownState !== null, ShowRotation: Plot !== null && DisplaySettings .ShowPlotRotation && !IsPreview, OnRotate: () => { RotateGardenPlot( PlotIndex ); } }); const Tile = View.Tile; const MainButton = View.MainButton; if (Plot === null) { MainButton.title = IsPreview ? "Future Sight: empty plot" : "Empty plot"; if (IsPreview) { MainButton.disabled = true; return Tile; } if ( SelectedGardenTool === "Trowel" ) { MainButton.addEventListener( "click", () => { PlantSeed( PlotIndex ); } ); } else if ( SelectedGardenTool === "MagicTrowel" ) { Tile.classList.add( "GardenPlotMagicPlant" ); MainButton.title = "Fill empty plots with the selected seed"; MainButton.addEventListener( "click", PlantManySeeds ); } else { MainButton.disabled = true; } return Tile; } if (Plant === undefined) { MainButton.disabled = true; return Tile; } if (IsPreview) { MainButton.title = "Future Sight: " + Plant.Name; MainButton.disabled = true; return Tile; } if ( SelectedGardenTool === "Shovel" ) { Tile.classList.add( "GardenPlotRemove" ); MainButton.title = "Remove " + Plant.Name; MainButton.addEventListener( "click", () => { RemovePlant( PlotIndex ); } ); return Tile; } if ( SelectedGardenTool === "Fertilizer" ) { if (Progress >= 1) { MainButton.title = Plant.Name + " is already fully grown"; MainButton.disabled = true; return Tile; } Tile.classList.add( "GardenPlotFertilize" ); MainButton.title = "Fertilize " + Plant.Name + " (" + GetFertilizerGrowthMinutes( GameSave ) + " minutes)"; MainButton.addEventListener( "click", () => { FertilizePlant( PlotIndex ); } ); return Tile; } if ( SelectedGardenTool === "MagicTrowel" ) { if (Progress < 1) { MainButton.title = Plant.Name + " - " + Math.floor( Progress * 100 ) + "% grown"; MainButton.disabled = true; return Tile; } Tile.classList.add( "GardenPlotMature", "GardenPlotMagicPlant" ); MainButton.title = "Harvest all mature " + Plant.Name + " plants in this Garden"; MainButton.addEventListener( "click", () => { HarvestManyPlants( Plot.Plant ); } ); return Tile; } if ( SelectedGardenTool !== "Trowel" ) { MainButton.title = Plant.Name + " - " + Math.floor( Progress * 100 ) + "% grown"; MainButton.disabled = true; return Tile; } if ( Progress >= 1 ) { Tile.classList.add( "GardenPlotMature" ); MainButton.title = "Harvest " + Plant.Name; MainButton.addEventListener( "click", () => { HarvestPlant( PlotIndex ); } ); } else { MainButton.title = Plant.Name + " - " + Math.floor( Progress * 100 ) + "% grown"; MainButton.disabled = true; } return Tile; } async function PlantSeed( PlotIndex ) { const ActionTime = Date.now(); AdvanceGameSimulation( GameSave, ActionTime ); if ( GameSave.Garden.Plots[ PlotIndex ] !== null ) { return; } 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 PlantResult = PlantGardenPlot( GameSave, GameSave.ActiveGardenIndex, PlotIndex, PlantKey, ActionTime, { ConsumeSeed: true, Rotation: GardenPlotDefaultRotation } ); if (!PlantResult.Planted) { EnsureSelectedSeed(); RenderSeeds(); return; } SetGardenMessage( "Planted " + Plant.Name + "." ); CheckGardenMutations( GameSave, ActionTime ); RenderGame(); await SaveGame( GameSave ); } async function PlantManySeeds() { const ActionTime = Date.now(); AdvanceGameSimulation( GameSave, ActionTime ); 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; } let PlantedCount = 0; for ( let Index = 0; Index < PlantCount; Index++ ) { const PlantResult = PlantGardenPlot( GameSave, GameSave.ActiveGardenIndex, EmptyPlotIndexes[Index], PlantKey, ActionTime, { ConsumeSeed: true, Rotation: GardenPlotDefaultRotation } ); if (!PlantResult.Planted) { break; } PlantedCount++; } if (PlantedCount <= 0) { EnsureSelectedSeed(); RenderSeeds(); return; } SetGardenMessage( "Magic trowel planted " + PlantedCount.toLocaleString() + " " + Plant.Name + (PlantedCount === 1 ? " seed." : " seeds.") ); CheckGardenMutations( GameSave, ActionTime ); RenderGame(); await SaveGame( GameSave ); } async function HarvestManyPlants( PlantKey ) { const ActionTime = Date.now(); AdvanceGameSimulation( GameSave, ActionTime ); const Plant = Plants[PlantKey]; if (Plant === undefined) { return; } const MatchingPlotIndexes = []; GameSave.Garden.Plots.forEach( (Plot, PlotIndex) => { if ( Plot !== null && Plot.Plant === PlantKey && IsPlantMature( Plot, Plant ) ) { MatchingPlotIndexes.push( PlotIndex ); } } ); if (MatchingPlotIndexes.length === 0) { SetGardenMessage( "There aren't any mature " + Plant.Name + " plants to harvest." ); RenderGame(); return; } let HarvestedCount = 0; let RewardAmount = 0; let DisabledCount = 0; let RewardUnavailableCount = 0; for ( const PlotIndex of MatchingPlotIndexes ) { const HarvestResult = HarvestGardenPlot( GameSave, GameSave.ActiveGardenIndex, PlotIndex, ActionTime ); if (!HarvestResult.Harvested) { if (HarvestResult.Disabled) { DisabledCount++; } else if ( HarvestResult.RewardUnavailable ) { RewardUnavailableCount++; } continue; } HarvestedCount++; RewardAmount += HarvestResult.RewardAmount; } if (HarvestedCount <= 0) { if (DisabledCount > 0) { SetGardenMessage( Plant.Name + " can't be harvested right now." ); } else if ( RewardUnavailableCount > 0 ) { SetGardenMessage( "Couldn't calculate the harvest reward for " + Plant.Name + "." ); } RenderGame(); return; } SetGardenMessage( "Magic trowel harvested " + HarvestedCount.toLocaleString() + " " + Plant.Name + (HarvestedCount === 1 ? " plant" : " plants") + " for " + RewardAmount.toLocaleString() + " Dew." ); CheckGardenMutations( GameSave, ActionTime ); RenderGame(); await SaveGame( GameSave ); } async function HarvestPlant( PlotIndex ) { const ActionTime = Date.now(); AdvanceGameSimulation( GameSave, ActionTime ); const Plot = GameSave.Garden.Plots[ PlotIndex ]; if (Plot === null) { return; } const Plant = Plants[Plot.Plant]; if ( Plant === undefined || !IsPlantMature( Plot, Plant ) ) { return; } const HarvestResult = HarvestGardenPlot( GameSave, GameSave.ActiveGardenIndex, PlotIndex, ActionTime ); if (!HarvestResult.Harvested) { if (HarvestResult.Disabled) { SetGardenMessage( Plant.Name + " can't be harvested right now." ); } else if ( HarvestResult.RewardUnavailable ) { SetGardenMessage( "Couldn't calculate the harvest reward for " + Plant.Name + "." ); } RenderGame(); return; } SetGardenMessage( "Harvested " + Plant.Name + " for " + HarvestResult.RewardAmount + " Dew." ); CheckGardenMutations( GameSave, ActionTime ); RenderGame(); await SaveGame( GameSave ); } async function RemovePlant( PlotIndex ) { const ActionTime = Date.now(); AdvanceGameSimulation( GameSave, ActionTime ); const Plot = GameSave.Garden.Plots[ PlotIndex ]; 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, ActionTime ); RenderGame(); await SaveGame( GameSave ); } async function FertilizePlant( PlotIndex ) { const ActionTime = Date.now(); AdvanceGameSimulation( GameSave, ActionTime ); 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, ActionTime ); RenderGame(); await SaveGame( GameSave ); } async function UseFutureSight() { const ActionTime = Date.now(); AdvanceGameSimulation( GameSave, ActionTime ); 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() { if (MutationCheckPending) { return; } MutationCheckPending = true; try { const SimulationResult = AdvanceGameSimulation( GameSave, Date.now() ); if ( SimulationResult.Attempted && FutureSightPreview !== null ) { FutureSightPreview = null; if ( SelectedGardenTool === "FutureSight" ) { SelectedGardenTool = "Trowel"; } } RenderGame(); if (SimulationResult.Dirty) { await SaveGame( GameSave ); } } finally { MutationCheckPending = false; } } function GiveReward( Reward ) { GameSave.Currency[ Reward.Currency ] ??= 0; GameSave.Currency[ Reward.Currency ] += Reward.Amount; GameSave.Statistics ??= {}; GameSave.Statistics.CurrencyEarned ??= {}; GameSave.Statistics.CurrencyEarned[ Reward.Currency ] ??= 0; GameSave.Statistics.CurrencyEarned[ Reward.Currency ] += Reward.Amount; } function GetPlantGrowthProgress( Plot, Plant ) { if ( Plant.GrowthTime <= 0 ) { return 1; } const Age = GetSimulationTime( GameSave ) - Plot.PlantedAt; return Math.max( 0, Math.min( Age / Plant.GrowthTime, 1 ) ); } function GetPlantTags( Plot, Plant ) { return [ ...(Plant.Tags ?? []) ]; } function IsPlantMature( Plot, Plant ) { return ( GetPlantGrowthProgress( Plot, Plant ) >= 1 ); } function GetPlantImages( Plot, Plant ) { const Direction = Plot !== null && Plant?.DirectionalSprites === true ? GetGardenPlotRotation( Plot ) : null; return GetPlantImageSources( Plant ?? Plot.Plant, Direction ); } function GetPlantImage( Plot, Plant ) { const Images = GetPlantImages( Plot, Plant ); if (Images.length === 0) { return null; } if (Images.length === 1) { return Images[0]; } const Progress = GetPlantGrowthProgress( Plot, Plant ); if (Progress >= 1) { return Images[ Images.length - 1 ]; } const GrowingImageCount = Images.length - 1; const ImageIndex = Math.min( Math.floor( Progress * GrowingImageCount ), GrowingImageCount - 1 ); return Images[ ImageIndex ]; } function SetGardenMessage( Message ) { document.getElementById( "GardenMessage" ).textContent = Message; } function GetNextHarvestInfo() { let NextHarvest = null; for ( const Plot of GameSave.Garden.Plots ) { if (Plot === null) { continue; } const Plant = Plants[Plot.Plant]; if (Plant === undefined) { continue; } const HarvestTime = Plot.PlantedAt + Plant.GrowthTime; if ( NextHarvest === null || HarvestTime < NextHarvest.Time ) { NextHarvest = { Time: HarvestTime, Plant }; } } return NextHarvest; } function RenderNextHarvest() { const Line = document.getElementById( "NextHarvestLine" ); const Element = document.getElementById( "NextHarvest" ); if ( Line === null || Element === null ) { return; } const HasOverview = HasGardenOverviewUpgrade( GameSave ); const ShowNextHarvest = !HasOverview || GameSave.Preferences .ShowNextHarvest !== false; Line.hidden = !ShowNextHarvest; if (!ShowNextHarvest) { return; } const Harvest = GetNextHarvestInfo(); if (Harvest === null) { Element.textContent = "Nothing planted"; Element.removeAttribute( "title" ); return; } const Remaining = Harvest.Time - GetSimulationTime( GameSave ); const IncludePlantName = HasOverview; if (Remaining <= 0) { Element.textContent = IncludePlantName ? Harvest.Plant.Name + " is ready!" : "Ready now!"; Element.removeAttribute( "title" ); return; } const RemainingText = FormatRemainingTime( Remaining ); Element.textContent = IncludePlantName ? Harvest.Plant.Name + " in " + RemainingText : RemainingText; const HarvestDate = new Date( Harvest.Time ); Element.title = "Ready at " + HarvestDate.toLocaleTimeString( [], { hour: "2-digit", minute: "2-digit" } ); } function RenderGardenOverview() { const Overview = document.getElementById( "GardenOverviewDetails" ); if (Overview === null) { return; } if ( !HasGardenOverviewUpgrade( GameSave ) ) { Overview.hidden = true; return; } const TotalPlots = GameSave.Garden.Width * GameSave.Garden.Height; let PlantedPlots = 0; let GrowingPlots = 0; let ReadyPlots = 0; for ( const Plot of GameSave.Garden.Plots ) { if (Plot === null) { continue; } PlantedPlots++; const Plant = Plants[Plot.Plant]; if (Plant === undefined) { continue; } if ( IsPlantMature( Plot, Plant ) ) { ReadyPlots++; } else { GrowingPlots++; } } const EmptyPlots = Math.max( 0, TotalPlots - PlantedPlots ); const Lines = [ { LineId: "GardenSizeOverviewLine", ValueId: "GardenSizeOverview", Preference: "ShowGardenSize", Value: GameSave.Garden.Width + "×" + GameSave.Garden.Height + " (" + TotalPlots.toLocaleString() + ")" }, { LineId: "EmptyPlotsOverviewLine", ValueId: "EmptyPlotsOverview", Preference: "ShowEmptyPlots", Value: EmptyPlots.toLocaleString() + "/" + TotalPlots.toLocaleString() }, { LineId: "PlantedPlotsOverviewLine", ValueId: "PlantedPlotsOverview", Preference: "ShowPlantedPlots", Value: PlantedPlots.toLocaleString() + "/" + TotalPlots.toLocaleString() }, { LineId: "GrowingPlotsOverviewLine", ValueId: "GrowingPlotsOverview", Preference: "ShowGrowingPlots", Value: GrowingPlots.toLocaleString() + "/" + TotalPlots.toLocaleString() }, { LineId: "ReadyPlotsOverviewLine", ValueId: "ReadyPlotsOverview", Preference: "ShowReadyPlots", Value: ReadyPlots.toLocaleString() + "/" + TotalPlots.toLocaleString() } ]; let VisibleLines = 0; for ( const Line of Lines ) { const LineElement = document.getElementById( Line.LineId ); const ValueElement = document.getElementById( Line.ValueId ); if ( LineElement === null || ValueElement === null ) { continue; } const IsVisible = GameSave.Preferences[ Line.Preference ] !== false; LineElement.hidden = !IsVisible; ValueElement.textContent = Line.Value; if (IsVisible) { VisibleLines++; } } Overview.hidden = VisibleLines === 0; } function FormatRemainingTime( Milliseconds ) { const TotalSeconds = Math.ceil( Milliseconds / 1000 ); const Days = Math.floor( TotalSeconds / 86400 ); const Hours = Math.floor( ( TotalSeconds % 86400 ) / 3600 ); const Minutes = Math.floor( ( TotalSeconds % 3600 ) / 60 ); const Seconds = TotalSeconds % 60; if (Days > 0) { return ( Days + "d " + Hours + "h" ); } if (Hours > 0) { return ( Hours + "h " + Minutes + "m" ); } if (Minutes > 0) { return ( Minutes + "m " + Seconds + "s" ); } return ( Seconds + "s" ); } function RenderGardenEconomy() { const Details = document.getElementById( "GardenEconomyDetails" ); if (Details === null) { return; } if ( !HasGardenEconomyUpgrade( GameSave ) ) { Details.hidden = true; return; } Details.hidden = false; const Summary = GetGardenEconomySummary( GameSave ); const Values = { GardenDewInvested: Summary === null ? "Unavailable" : Summary.DewInvested .toLocaleString(), GardenHarvestValue: Summary === null ? "Unavailable" : Summary.HarvestValue .toLocaleString(), GardenNetProfit: Summary === null ? "Unavailable" : Summary.NetProfit .toLocaleString(), GardenDewPerHour: Summary === null ? "Unavailable" : FormatDewPerHour( Summary.DewPerHour ) }; for ( const [ElementId, Value] of Object.entries(Values) ) { const Element = document.getElementById( ElementId ); if (Element !== null) { Element.textContent = Value; } } } function RenderGardenInfoLayout() { const DetailRow = document.getElementById( "GardenInfoDetailRow" ); const OverviewSection = document.getElementById( "GardenOverviewSection" ); const EconomySection = document.getElementById( "GardenEconomySection" ); if ( DetailRow === null || OverviewSection === null || EconomySection === null ) { return; } const HasOverview = HasGardenOverviewUpgrade( GameSave ); const HasEconomy = HasGardenEconomyUpgrade( GameSave ); DetailRow.hidden = !HasOverview && !HasEconomy; OverviewSection.classList.toggle( "GardenInfoSectionEmpty", !HasOverview ); EconomySection.classList.toggle( "GardenInfoSectionEmpty", !HasEconomy ); } document.addEventListener( "DOMContentLoaded", StartGame );