let GardenerData = null; let GardenerViewerProfile = null; let GardenerGardenIndex = 0; let GardenerCommentOffset = 0; const GardenerCommentPageSize = 20; async function StartGardener() { const Query = new URLSearchParams( window.location.search ); const Username = Query.get("Username")?.trim() ?? ""; if (Username.length === 0) { SetGardenerPageError( "No gardener was selected." ); return; } BindGardenerControls(); try { await LoadGameContent(); } catch (Error) { console.error( "Couldn't load Garden content for gardener profile:", Error ); } try { const [Gardener, ViewerProfile] = await Promise.all([ FetchGardener( Username ), GetProfile().catch( () => null ) ]); if (!Gardener.Success) { throw new Error( Gardener.Error ?? "Couldn't load gardener." ); } if (!Gardener.Exists) { SetGardenerPageError( "That gardener does not exist." ); return; } GardenerData = Gardener; GardenerViewerProfile = ViewerProfile?.Success && ViewerProfile.Exists ? ViewerProfile : null; GardenerGardenIndex = Number.isInteger( Number( Gardener.ActiveGardenIndex ) ) ? Number( Gardener.ActiveGardenIndex ) : 0; RenderGardener(); if ( Gardener.Permissions?.Comments !== false ) { await LoadGardenerComments( true ); } } catch (Error) { console.error( "Couldn't load gardener:", Error ); SetGardenerPageError( "Couldn't load gardener." ); } } function BindGardenerControls() { document.getElementById( "PreviousGardenerGardenButton" )?.addEventListener( "click", () => ChangeGardenerGarden(-1) ); document.getElementById( "NextGardenerGardenButton" )?.addEventListener( "click", () => ChangeGardenerGarden(1) ); document.getElementById( "GardenerDonationForm" )?.addEventListener( "submit", SubmitGardenerDonation ); document.getElementById( "GardenerCommentForm" )?.addEventListener( "submit", SubmitGardenerComment ); document.getElementById( "LoadMoreGardenerCommentsButton" )?.addEventListener( "click", () => LoadGardenerComments(false) ); } async function FetchGardener( Username ) { const Response = await fetch( ApiUrl + "/Gardener.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ SaveKey: GetSaveKey(), Username: Username }) } ); return await Response.json(); } function RenderGardener() { RenderGardenerHeader(); RenderGardenerStatistics(); RenderGardenerGardens(); RenderGardenerDonation(); RenderGardenerCommentsAvailability(); const Message = document.getElementById( "GardenerPageMessage" ); if (Message !== null) { Message.hidden = true; } } function RenderGardenerHeader() { const AvatarContainer = document.getElementById( "GardenerAvatar" ); const Name = document.getElementById( "GardenerName" ); const Description = document.getElementById( "GardenerDescription" ); if ( AvatarContainer === null || Name === null || Description === null || GardenerData === null ) { return; } AvatarContainer.replaceChildren( CreateGardenerAvatar( GardenerData.ProfilePicture, GardenerData.Username, "GardenerAvatarHero" ) ); Name.textContent = GardenerData.Username; ApplyPlayerColour( Name, GardenerData.Colour ); Description.textContent = typeof GardenerData.Description === "string" && GardenerData.Description.length > 0 ? GardenerData.Description : "No description."; } function RenderGardenerStatistics() { const Section = document.getElementById( "GardenerStatsSection" ); if ( Section === null || GardenerData === null ) { return; } Section.hidden = false; const Stats = GardenerData.Stats; if ( Stats === null || typeof Stats !== "object" ) { Section.replaceChildren( CreateGardenerPrivateSection( "Statistics", "This gardener keeps their statistics private." ) ); return; } SetGardenerStatistic( "GardenerCurrentDew", Stats.CurrentDew ); SetGardenerStatistic( "GardenerLifetimeDew", Stats.LifetimeDew ); SetGardenerStatistic( "GardenerPlantsDiscovered", Stats.PlantsDiscovered ); SetGardenerStatistic( "GardenerMutationsDiscovered", Stats.MutationsDiscovered ); SetGardenerStatistic( "GardenerGardensOwned", Stats.GardensOwned ); const GardenSize = Number(Stats.ActiveGardenWidth) + "×" + Number(Stats.ActiveGardenHeight); const GardenSizeElement = document.getElementById( "GardenerActiveGardenSize" ); if (GardenSizeElement !== null) { GardenSizeElement.textContent = GardenSize; } } function SetGardenerStatistic( ElementId, Value ) { const Element = document.getElementById( ElementId ); if (Element !== null) { Element.textContent = Number(Value ?? 0) .toLocaleString(); } } function CreateGardenerPrivateSection( Title, Message ) { const Fragment = document.createDocumentFragment(); const Heading = document.createElement( "h2" ); Heading.className = "PanelHeader PanelHeaderInset"; Heading.textContent = Title; const Text = document.createElement( "p" ); Text.className = "ProfileMessage"; Text.textContent = Message; Fragment.append( Heading, Text ); return Fragment; } function RenderGardenerGardens() { const Section = document.getElementById( "GardenerGardensSection" ); if ( Section === null || GardenerData === null ) { return; } Section.hidden = false; if (!Array.isArray(GardenerData.Gardens)) { Section.replaceChildren( CreateGardenerPrivateSection( "Gardens", "This gardener has disabled Garden previews." ) ); return; } if (GardenerData.Gardens.length === 0) { const Message = document.getElementById( "GardenerGardensMessage" ); if (Message !== null) { Message.textContent = "This gardener has no Gardens to preview."; } document.getElementById( "GardenerGardenGrid" )?.replaceChildren(); return; } GardenerGardenIndex = Math.max( 0, Math.min( GardenerData.Gardens.length - 1, GardenerGardenIndex ) ); RenderCurrentGardenerGarden(); } function ChangeGardenerGarden( Difference ) { if ( !Array.isArray( GardenerData?.Gardens ) || GardenerData.Gardens.length <= 1 ) { return; } GardenerGardenIndex = ( GardenerGardenIndex + Difference + GardenerData.Gardens.length ) % GardenerData.Gardens.length; RenderCurrentGardenerGarden(); } function RenderCurrentGardenerGarden() { const Gardens = GardenerData?.Gardens; if ( !Array.isArray(Gardens) || Gardens.length === 0 ) { return; } const Garden = Gardens[GardenerGardenIndex]; const Grid = document.getElementById( "GardenerGardenGrid" ); const Name = document.getElementById( "GardenerGardenName" ); const Size = document.getElementById( "GardenerGardenSize" ); const Position = document.getElementById( "GardenerGardenPosition" ); const PreviousButton = document.getElementById( "PreviousGardenerGardenButton" ); const NextButton = document.getElementById( "NextGardenerGardenButton" ); const Message = document.getElementById( "GardenerGardensMessage" ); if (Grid === null) { return; } Grid.style.setProperty( "--GardenWidth", Math.max( 1, Number(Garden.Width) || 1 ) ); Grid.replaceChildren(); for ( const Plot of Array.isArray(Garden.Plots) ? Garden.Plots : [] ) { Grid.appendChild( CreateGardenerGardenPlot( Plot ) ); } if (Name !== null) { Name.textContent = Garden.Name ?? "Garden"; } if (Size !== null) { Size.textContent = Number(Garden.Width) + "×" + Number(Garden.Height); } if (Position !== null) { Position.textContent = (GardenerGardenIndex + 1) + " / " + Gardens.length; } const HasMultiple = Gardens.length > 1; if (PreviousButton !== null) { PreviousButton.disabled = !HasMultiple; } if (NextButton !== null) { NextButton.disabled = !HasMultiple; } if (Message !== null) { Message.textContent = "Read-only Garden preview."; } } function CreateGardenerGardenPlot( Plot ) { const Tile = document.createElement( "div" ); Tile.className = "PlantTile GardenPlot GardenPlotReadOnly"; const Main = document.createElement( "button" ); Main.type = "button"; Main.className = "GardenPlotMain"; Main.disabled = true; const Visual = document.createElement( "span" ); Visual.className = "GardenPlotVisual"; if ( Plot === null || typeof Plot !== "object" ) { Tile.classList.add( "GardenPlotEmpty" ); Main.title = "Empty plot"; Main.appendChild( Visual ); Tile.appendChild( Main ); return Tile; } const Plant = Plants[Plot.Plant]; Tile.classList.add( "GardenPlotWithName" ); const PlantName = document.createElement( "span" ); PlantName.className = "GardenPlotName"; PlantName.textContent = Plant?.Name ?? Plot.Plant ?? "Unknown"; Main.appendChild( PlantName ); if (Plant === undefined) { Visual.textContent = "?"; Main.title = "Unknown plant"; Main.appendChild( Visual ); Tile.appendChild( Main ); return Tile; } const Progress = GetGardenerGardenPlantProgress( Plot, Plant ); Tile.style.setProperty( "--GrowthProgress", (Progress * 100) + "%" ); if (Progress >= 1) { Tile.classList.add( "GardenPlotMature" ); } const ImageSource = GetGardenerGardenPlantImage( Plot, Plant, Progress ); if (ImageSource !== null) { const Image = document.createElement( "img" ); Image.className = "PlantSprite"; Image.src = ImageSource; Image.alt = ""; Image.draggable = false; Visual.appendChild( Image ); } Main.title = Plant.Name + (Progress >= 1 ? " — mature" : " — growing"); Main.appendChild( Visual ); Tile.appendChild( Main ); return Tile; } function GetGardenerGardenPlantProgress( Plot, Plant ) { const GrowthTime = Number(Plant.GrowthTime ?? 0); if (GrowthTime <= 0) { return 1; } const PlantedAt = Number(Plot.PlantedAt ?? 0); const Age = Date.now() - PlantedAt; return Math.max( 0, Math.min( Age / GrowthTime, 1 ) ); } function GetGardenerGardenPlantImage( Plot, Plant, Progress ) { const Direction = Plant.DirectionalSprites === true ? Plot.Rotation ?? "East" : null; const Images = GetPlantImageSources( Plant, Direction ); if (Images.length === 0) { return null; } if (Images.length === 1) { return Images[0] ?? null; } if (Progress >= 1) { return Images[ Images.length - 1 ] ?? null; } const GrowingImageCount = Images.length - 1; const Index = Math.min( Math.floor( Progress * GrowingImageCount ), GrowingImageCount - 1 ); return Images[Index] ?? null; } function RenderGardenerDonation() { const Section = document.getElementById( "GardenerDonationSection" ); const Form = document.getElementById( "GardenerDonationForm" ); const Message = document.getElementById( "GardenerDonationMessage" ); if ( Section === null || Form === null || Message === null || GardenerData === null ) { return; } Section.hidden = false; if ( GardenerData.Permissions ?.DewDonations === false ) { Form.hidden = true; SetGardenerDonationBalance(null); Message.textContent = "This gardener has disabled Dew donations."; return; } if (GardenerData.IsSelf === true) { Form.hidden = true; SetGardenerDonationBalance( GardenerViewerProfile?.CurrentDew ?? null ); Message.textContent = "You cannot donate Dew to yourself."; return; } if (GardenerViewerProfile === null) { Form.hidden = true; SetGardenerDonationBalance(null); Message.textContent = "Create a Garden save before donating Dew."; return; } Form.hidden = false; Message.textContent = ""; SetGardenerDonationBalance( GardenerViewerProfile.CurrentDew ); } function SetGardenerDonationBalance( Dew ) { const Balance = document.getElementById( "GardenerDonationBalance" ); if (Balance === null) { return; } if (!Number.isFinite(Number(Dew))) { Balance.textContent = ""; return; } Balance.textContent = "You have " + Number(Dew).toLocaleString() + " Dew available."; } async function SubmitGardenerDonation( Event ) { Event.preventDefault(); if (GardenerData === null) { return; } const Input = document.getElementById( "GardenerDonationAmount" ); const Message = document.getElementById( "GardenerDonationMessage" ); const Button = Event.currentTarget.querySelector( "button[type='submit']" ); const Amount = Number( Input?.value ); if ( !Number.isInteger(Amount) || Amount < 1 ) { if (Message !== null) { Message.textContent = "Enter a positive whole number of Dew."; } return; } if (Button !== null) { Button.disabled = true; } if (Message !== null) { Message.textContent = "Donating..."; } try { const Response = await fetch( ApiUrl + "/DonateDew.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ SaveKey: GetSaveKey(), Username: GardenerData.Username, Amount: Amount }) } ); const Result = await Response.json(); if (!Result.Success) { throw new Error( Result.Error ?? "Couldn't donate Dew." ); } if ( Result.SaveData !== null && typeof Result.SaveData === "object" ) { WriteLocalSave( NormalizeSaveData( Result.SaveData ) ); } if (GardenerViewerProfile !== null) { GardenerViewerProfile.CurrentDew = Number(Result.CurrentDew); } if ( GardenerData.Stats !== null && typeof GardenerData.Stats === "object" && Number.isFinite( Number(Result.TargetCurrentDew) ) ) { GardenerData.Stats.CurrentDew = Number( Result.TargetCurrentDew ); RenderGardenerStatistics(); } SetGardenerDonationBalance( Result.CurrentDew ); if (Input !== null) { Input.value = ""; } if (Message !== null) { Message.textContent = "Donated " + Amount.toLocaleString() + " Dew to " + GardenerData.Username + "."; } } catch (Error) { console.error( "Couldn't donate Dew:", Error ); if (Message !== null) { Message.textContent = Error.message ?? "Couldn't donate Dew."; } } finally { if (Button !== null) { Button.disabled = false; } } } function RenderGardenerCommentsAvailability() { const Section = document.getElementById( "GardenerCommentsSection" ); const Form = document.getElementById( "GardenerCommentForm" ); const Message = document.getElementById( "GardenerCommentMessage" ); if ( Section === null || Form === null || Message === null || GardenerData === null ) { return; } Section.hidden = false; if ( GardenerData.Permissions ?.Comments === false ) { Form.hidden = true; Message.textContent = "This gardener has disabled comments."; document.getElementById( "GardenerCommentList" )?.replaceChildren(); return; } if ( GardenerViewerProfile === null || typeof GardenerViewerProfile.Username !== "string" || GardenerViewerProfile.Username.length === 0 ) { Form.hidden = true; Message.textContent = "Set a username before leaving comments."; return; } Form.hidden = false; Message.textContent = ""; } async function LoadGardenerComments( Reset ) { if (GardenerData === null) { return; } const List = document.getElementById( "GardenerCommentList" ); const LoadMoreButton = document.getElementById( "LoadMoreGardenerCommentsButton" ); if (List === null) { return; } if (Reset) { GardenerCommentOffset = 0; List.replaceChildren(); } if (LoadMoreButton !== null) { LoadMoreButton.disabled = true; } try { const Result = await GetGardenerComments( GardenerData.Username, GardenerCommentOffset, GardenerCommentPageSize ); if (!Result.Success) { throw new Error( Result.Error ?? "Couldn't load comments." ); } if (!Result.Enabled) { RenderGardenerCommentsAvailability(); return; } const Comments = Array.isArray(Result.Comments) ? Result.Comments : []; for (const Comment of Comments) { List.appendChild( CreateGardenerComment( Comment ) ); } GardenerCommentOffset += Comments.length; if ( Reset && Comments.length === 0 ) { const Empty = document.createElement( "p" ); Empty.className = "GardenerCommentsEmpty"; Empty.textContent = "No comments yet."; List.appendChild( Empty ); } if (LoadMoreButton !== null) { LoadMoreButton.hidden = Result.HasMore !== true; } } catch (Error) { console.error( "Couldn't load gardener comments:", Error ); const Message = document.getElementById( "GardenerCommentMessage" ); if (Message !== null) { Message.textContent = "Couldn't load comments."; } } finally { if (LoadMoreButton !== null) { LoadMoreButton.disabled = false; } } } function CreateGardenerComment( Comment ) { return CreateGardenerCommentCard( Comment, DeleteGardenerComment ); } async function SubmitGardenerComment( Event ) { Event.preventDefault(); if (GardenerData === null) { return; } const Input = document.getElementById( "GardenerCommentInput" ); const Message = document.getElementById( "GardenerCommentMessage" ); const Button = Event.currentTarget.querySelector( "button[type='submit']" ); const Text = Input?.value.trim() ?? ""; if (Text.length === 0) { if (Message !== null) { Message.textContent = "Comment cannot be empty."; } return; } if (Button !== null) { Button.disabled = true; } if (Message !== null) { Message.textContent = "Posting..."; } try { const Response = await fetch( ApiUrl + "/Comments.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ Action: "Add", SaveKey: GetSaveKey(), Username: GardenerData.Username, Text: Text }) } ); const Result = await Response.json(); if (!Result.Success) { throw new Error( Result.Error ?? "Couldn't leave comment." ); } if (Input !== null) { Input.value = ""; } if (Message !== null) { Message.textContent = "Comment posted."; } await LoadGardenerComments( true ); } catch (Error) { console.error( "Couldn't leave comment:", Error ); if (Message !== null) { Message.textContent = Error.message ?? "Couldn't leave comment."; } } finally { if (Button !== null) { Button.disabled = false; } } } async function DeleteGardenerComment( CommentId, Button ) { Button.disabled = true; const Message = document.getElementById( "GardenerCommentMessage" ); try { const Result = await DeleteGardenerCommentRequest( CommentId ); if (!Result.Success) { throw new Error( Result.Error ?? "Couldn't delete comment." ); } if (Message !== null) { Message.textContent = "Comment deleted."; } await LoadGardenerComments( true ); } catch (Error) { console.error( "Couldn't delete comment:", Error ); if (Message !== null) { Message.textContent = Error.message ?? "Couldn't delete comment."; } Button.disabled = false; } } function SetGardenerPageError( Message ) { const PageMessage = document.getElementById( "GardenerPageMessage" ); if (PageMessage !== null) { PageMessage.hidden = false; PageMessage.textContent = Message; } const Description = document.getElementById( "GardenerDescription" ); if (Description !== null) { Description.textContent = ""; } } document.addEventListener( "DOMContentLoaded", StartGardener );