let GameSave; let SelectedSeedId = null; let SelectedGardenTool = "Seed"; let MutationCheckPending = false; 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(); BindGardenTools(); EnsureSelectedSeed(); const InitialMutationResult = CheckGardenMutations( GameSave ); RenderGame(); if (InitialMutationResult.Attempted) { await SaveGame( GameSave ); } setInterval( GardenTick, GardenUpdateInterval ); await RenderUser(); await RenderLeaderboard(); } function RenderGame() { EnsureSelectedSeed(); RenderCurrency(); RenderSeeds(); RenderTools(); RenderGarden(); RenderNextHarvest(); } function BindGardenTools() { const ShovelButton = document.getElementById( "ShovelToolButton" ); if (ShovelButton === null) { return; } ShovelButton.addEventListener( "click", () => { if ( SelectedGardenTool === "Shovel" ) { SelectedGardenTool = null; } else { SelectedGardenTool = "Shovel"; } RenderSeeds(); RenderTools(); RenderGarden(); } ); } function RenderTools() { const ShovelButton = document.getElementById( "ShovelToolButton" ); if (ShovelButton === null) { return; } if ( SelectedGardenTool === "Shovel" ) { ShovelButton.setAttribute( "aria-current", "true" ); } else { ShovelButton.removeAttribute( "aria-current" ); } } 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; } if ( SelectedGardenTool === "Seed" ) { SelectedGardenTool = null; } } 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 ( SelectedGardenTool === "Seed" && PlantId === SelectedSeedId ) { Button.setAttribute( "aria-current", "true" ); } Button.addEventListener( "click", () => { SelectedGardenTool = "Seed"; SelectedSeedId = PlantId; RenderSeeds(); RenderTools(); RenderGarden(); SetGardenMessage( "Selected " + Plant.Name + "." ); } ); SeedList.appendChild( Button ); } } function RenderGarden() { const GardenGrid = document.getElementById( "GardenGrid" ); GardenGrid.style.setProperty( "--GardenWidth", GameSave.Garden.Width ); GardenGrid.replaceChildren(); GameSave.Garden.Plots.forEach( (Plot, PlotIndex) => { const PlotElement = CreatePlotElement( Plot, PlotIndex ); GardenGrid.appendChild( PlotElement ); } ); } function CreatePlotElement( Plot, PlotIndex ) { const Button = document.createElement( "button" ); Button.className = "PlantTile GardenPlot"; Button.type = "button"; if (Plot === null) { Button.classList.add( "GardenPlotEmpty" ); Button.title = "Empty plot"; if ( SelectedGardenTool === "Seed" ) { Button.addEventListener( "click", () => { PlantSeed( PlotIndex ); } ); } return Button; } const Plant = Plants[Plot.Plant]; if (Plant === undefined) { Button.textContent = "?"; return Button; } const Progress = GetPlantGrowthProgress( Plot, Plant ); Button.style.setProperty( "--GrowthProgress", `${Progress * 100}%` ); const ImagePath = GetPlantImage( Plot, Plant ); if (ImagePath !== null) { const Image = document.createElement( "img" ); Image.className = "PlantSprite"; Image.alt = Plant.Name; Image.src = ImagePath; Button.appendChild( Image ); } else { const MissingImage = document.createElement( "span" ); MissingImage.className = "GardenPlantMissing"; MissingImage.textContent = Plant.Name; Button.appendChild( MissingImage ); } if ( SelectedGardenTool === "Shovel" ) { Button.classList.add( "GardenPlotRemove" ); Button.title = "Remove " + Plant.Name; Button.addEventListener( "click", () => { RemovePlant( PlotIndex ); } ); return Button; } if ( Progress >= 1 ) { Button.classList.add( "GardenPlotMature" ); Button.title = "Harvest " + Plant.Name; Button.addEventListener( "click", () => { HarvestPlant( PlotIndex ); } ); } else { Button.title = Plant.Name + " - " + Math.floor( Progress * 100 ) + "% grown"; } return Button; } async function PlantSeed( PlotIndex ) { 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: Date.now() }; SetGardenMessage( "Planted " + Plant.Name + "." ); CheckGardenMutations( GameSave ); RenderGame(); await SaveGame( GameSave ); } async function HarvestPlant( PlotIndex ) { 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 ); RenderGame(); await SaveGame( GameSave ); } async function RemovePlant( PlotIndex ) { if ( GameSave.Garden.Plots[ PlotIndex ] === null ) { return; } GameSave.Garden.Plots[ PlotIndex ] = null; CheckGardenMutations( GameSave ); RenderGame(); await SaveGame( GameSave ); } async function GardenTick() { RenderGarden(); RenderNextHarvest(); if (MutationCheckPending) { return; } MutationCheckPending = true; try { const MutationResult = CheckGardenMutations( GameSave ); if (MutationResult.Attempted) { RenderGame(); 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 = Date.now() - 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 ) { return PlantImages[ Plot.Plant ] ?? []; } 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 GetNextHarvestTime() { let NextHarvestTime = 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 ( HarvestTime <= Date.now() ) { return Date.now(); } if ( NextHarvestTime === null || HarvestTime < NextHarvestTime ) { NextHarvestTime = HarvestTime; } } return NextHarvestTime; } function RenderNextHarvest() { const Element = document.getElementById( "NextHarvest" ); if (Element === null) { return; } const HarvestTime = GetNextHarvestTime(); if (HarvestTime === null) { Element.textContent = "Nothing planted"; Element.removeAttribute( "title" ); return; } const Remaining = HarvestTime - Date.now(); if (Remaining <= 0) { Element.textContent = "Ready now!"; Element.removeAttribute( "title" ); return; } Element.textContent = FormatRemainingTime( Remaining ); const HarvestDate = new Date( HarvestTime ); Element.title = "Ready at " + HarvestDate.toLocaleTimeString( [], { hour: "2-digit", minute: "2-digit" } ); } 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" ); } document.addEventListener( "DOMContentLoaded", StartGame );