let GameSave; let SelectedSeedId = null; let SelectedGardenTool = "Trowel"; let FutureSightPreview = null; let MutationCheckPending = false; let QuickBuyPurchasePending = false; let QuickBuyAmountMode = "One"; const GardenUpdateInterval = 1000; const GardenBorderColours = [ "var(--Rosewater)", "var(--Flamingo)", "var(--Pink)", "var(--Mauve)", "var(--Red)", "var(--Maroon)", "var(--Peach)", "var(--Yellow)", "var(--Green)", "var(--Teal)", "var(--Sky)", "var(--Sapphire)", "var(--Blue)", "var(--Lavender)" ]; const GardenPlotDefaultRotation = "East"; const GardenPlotRotations = [ "North", "East", "South", "West" ]; const GardenPlotRotationLabels = { North: "^", East: "->", South: "v", West: "<-" }; function GetGardenBorderColour( GardenIndex ) { const ColourCount = GardenBorderColours.length; const NormalizedIndex = ( Number(GardenIndex) % ColourCount + ColourCount ) % ColourCount; return GardenBorderColours[ NormalizedIndex ]; } 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() { 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; const HasPreviousGarden = GardenIndex > 0; const HasNextGarden = GardenIndex < GardenCount - 1; PreviousButton.disabled = !HasPreviousGarden; NextButton.disabled = !HasNextGarden; PreviousButton.style.setProperty( "--GardenSelectorBorderColour", HasPreviousGarden ? GetGardenBorderColour( GardenIndex - 1 ) : "var(--Surface2)" ); NameInput.style.setProperty( "--GardenSelectorBorderColour", GetGardenBorderColour( GardenIndex ) ); NextButton.style.setProperty( "--GardenSelectorBorderColour", HasNextGarden ? GetGardenBorderColour( GardenIndex + 1 ) : "var(--Surface2)" ); PreviousButton.setAttribute( "aria-label", HasPreviousGarden ? "Previous Garden: " + GameSave.Gardens[ GardenIndex - 1 ].Name : "No previous Garden" ); NextButton.setAttribute( "aria-label", HasNextGarden ? "Next Garden: " + GameSave.Gardens[ GardenIndex + 1 ].Name : "No next Garden" ); if ( document.activeElement !== NameInput ) { NameInput.value = GameSave.Garden.Name; } NameInput.title = "Garden " + (GardenIndex + 1) + " of " + GardenCount; } 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 }; } function GetGardenPlotRotation( Plot ) { return GardenPlotRotations.includes( Plot?.Rotation ) ? Plot.Rotation : GardenPlotDefaultRotation; } function GetGardenPlotRotationLabel( Plot ) { return GardenPlotRotationLabels[ GetGardenPlotRotation( Plot ) ] ?? "->"; } 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 Tile = document.createElement( "div" ); Tile.className = "PlantTile GardenPlot"; const MainButton = document.createElement( "button" ); MainButton.className = "GardenPlotMain"; MainButton.type = "button"; if ( DisplaySettings.ShowPlantNames ) { Tile.classList.add( "GardenPlotWithName" ); } const Plant = Plot === null ? null : Plants[Plot.Plant]; if ( DisplaySettings.ShowPlantNames ) { MainButton.appendChild( CreateGardenPlotName( Plot, Plant ) ); } const Visual = document.createElement( "span" ); Visual.className = "GardenPlotVisual"; MainButton.appendChild( Visual ); Tile.appendChild( MainButton ); if (Plot === null) { Tile.classList.add( "GardenPlotEmpty" ); MainButton.title = IsPreview ? "Future Sight: empty plot" : "Empty plot"; AppendGardenPlotFooter( Tile, null, null, 0, PlotIndex, DisplaySettings, IsPreview ); 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) { Visual.textContent = "?"; AppendGardenPlotFooter( Tile, Plot, null, 0, PlotIndex, DisplaySettings, IsPreview ); MainButton.disabled = true; return Tile; } const Progress = GetPlantGrowthProgress( Plot, Plant ); const CooldownState = Progress >= 1 && typeof GetFunctionalPlantCooldownDisplayState === "function" ? GetFunctionalPlantCooldownDisplayState( Plot, Plant, GetSimulationTime( GameSave ) ) : null; const BorderProgress = CooldownState === null ? Progress : CooldownState.Progress; Tile.style.setProperty( "--GrowthProgress", `${BorderProgress * 100}%` ); if (CooldownState !== null) { Tile.classList.add( "GardenPlotFunctionalCooldown" ); } const ImagePath = GetPlantImage( Plot, Plant ); if (ImagePath !== null) { const Image = document.createElement( "img" ); Image.className = "PlantSprite"; Image.alt = Plant.Name; Image.src = ImagePath; Visual.appendChild( Image ); } else { const MissingImage = document.createElement( "span" ); MissingImage.className = "GardenPlantMissing"; MissingImage.textContent = Plant.Name; Visual.appendChild( MissingImage ); } AppendGardenPlotFooter( Tile, Plot, Plant, Progress, PlotIndex, DisplaySettings, IsPreview ); 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 !== "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; } function AppendGardenPlotFooter( Tile, Plot, Plant, Progress, PlotIndex, DisplaySettings, IsPreview ) { const ShowTimer = DisplaySettings.ShowGrowthTimers; const ShowRotation = Plot !== null && DisplaySettings.ShowPlotRotation && !IsPreview; if ( !ShowTimer && !ShowRotation ) { return; } Tile.classList.add( "GardenPlotWithFooter" ); const Footer = document.createElement( "span" ); Footer.className = "GardenPlotFooter"; if ( ShowTimer && ShowRotation ) { Footer.classList.add( "GardenPlotFooterSplit" ); } if (ShowTimer) { Footer.appendChild( CreateGardenPlotTimer( Plot, Plant, Progress ) ); } if (ShowRotation) { const RotateButton = document.createElement( "button" ); RotateButton.className = "GardenPlotRotationButton"; RotateButton.type = "button"; RotateButton.textContent = GetGardenPlotRotationLabel( Plot ); RotateButton.title = "Rotate plot clockwise"; RotateButton.setAttribute( "aria-label", "Rotate " + (Plant?.Name ?? "plot") + " clockwise" ); RotateButton.addEventListener( "click", () => { RotateGardenPlot( PlotIndex ); } ); Footer.appendChild( RotateButton ); } Tile.appendChild(Footer); } function CreateGardenPlotName( Plot, Plant ) { const Name = document.createElement( "span" ); Name.className = "GardenPlotName"; if (Plot === null) { Name.textContent = "Empty"; } else if (Plant === undefined) { Name.textContent = "Unknown"; } else { Name.textContent = Plant.Name; Name.title = Plant.Name; } return Name; } function CreateGardenPlotTimer( Plot, Plant, Progress ) { const Timer = document.createElement( "span" ); Timer.className = "GardenPlotTimer"; if ( Plot === null || Plant === null || Plant === undefined ) { Timer.textContent = ""; return Timer; } if ( Progress >= 1 && typeof GetFunctionalPlantCooldownDisplayState === "function" ) { const CooldownState = GetFunctionalPlantCooldownDisplayState( Plot, Plant, GetSimulationTime( GameSave ) ); if (CooldownState !== null) { Timer.textContent = FormatGardenRemainingTime( CooldownState.Remaining ); return Timer; } } if (Progress >= 1) { Timer.textContent = "Ready"; return Timer; } const RemainingTime = Math.max( 0, Plant.GrowthTime - ( GetSimulationTime( GameSave ) - Plot.PlantedAt ) ); Timer.textContent = FormatGardenRemainingTime( RemainingTime ); return Timer; } function FormatGardenRemainingTime( Milliseconds ) { const TotalSeconds = Math.max( 0, Math.ceil( Number( Milliseconds ) / 1000 ) ); const Hours = Math.floor( TotalSeconds / 3600 ); const Minutes = Math.floor( ( TotalSeconds % 3600 ) / 60 ); const Seconds = TotalSeconds % 60; if (Hours > 0) { return ( Hours + ":" + String(Minutes).padStart( 2, "0" ) + ":" + String(Seconds).padStart( 2, "0" ) ); } return ( Minutes + ":" + String(Seconds).padStart( 2, "0" ) ); } async function PlantSeed( PlotIndex ) { 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; } if ( !TakeSeed( GameSave, SelectedSeedId ) ) { EnsureSelectedSeed(); RenderSeeds(); return; } GameSave.Garden.Plots[ PlotIndex ] = { Plant: PlantKey, PlantedAt: ActionTime, Rotation: GardenPlotDefaultRotation }; 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; } if ( !TakeSeed( GameSave, SelectedSeedId, PlantCount ) ) { return; } const PlantedAt = ActionTime; for ( let Index = 0; Index < PlantCount; Index++ ) { GameSave.Garden.Plots[ EmptyPlotIndexes[Index] ] = { Plant: PlantKey, PlantedAt: PlantedAt, Rotation: GardenPlotDefaultRotation }; } SetGardenMessage( "Magic trowel planted " + PlantCount.toLocaleString() + " " + Plant.Name + (PlantCount === 1 ? " seed." : " seeds.") ); 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 RewardAmount = GetPlantHarvestReward( GameSave, Plant.Id ); if (RewardAmount === null) { SetGardenMessage( "Couldn't calculate the harvest reward for " + Plant.Name + "." ); return; } GiveReward({ Currency: "Dew", Amount: RewardAmount }); GameSave.Garden.Plots[ PlotIndex ] = null; SetGardenMessage( "Harvested " + Plant.Name + " for " + 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 );