diff --git a/Pages/AdminPlants.php b/Pages/AdminPlants.php index 29f70fb..7e4cff7 100644 --- a/Pages/AdminPlants.php +++ b/Pages/AdminPlants.php @@ -246,6 +246,17 @@ $PageSection = "Admin"; + +
-+
+ Choose whether plot directions are shown + beside plot timers. +
+ ++ Unlock Plot rotation in the Shop + to use this setting. +
+diff --git a/Scripts/Garden/AdminMassPlantEditor.js b/Scripts/Garden/AdminMassPlantEditor.js index 6f70d80..4da42f6 100644 --- a/Scripts/Garden/AdminMassPlantEditor.js +++ b/Scripts/Garden/AdminMassPlantEditor.js @@ -402,6 +402,8 @@ function BuildAdminMassPlantPayload( Source.Effects ?? {} ) ), + DirectionalSprites: + Source.DirectionalSprites === true, ShopPlant: Source.Shop?.ShopPlant === true, BaseCost: diff --git a/Scripts/Garden/AdminOverview.js b/Scripts/Garden/AdminOverview.js index a2c2ed6..92bcbf1 100644 --- a/Scripts/Garden/AdminOverview.js +++ b/Scripts/Garden/AdminOverview.js @@ -95,10 +95,23 @@ function GetAdminOverviewMissingSprites( ) ); + const DirectionalStages = + Array.isArray( + Plant.DirectionalImageStages + ) + ? Plant.DirectionalImageStages + : []; + const HighestApiStage = Math.max( 0, - ...ApiStages + ...ApiStages, + ...DirectionalStages.map( + Stage => + Number( + Stage?.Stage ?? 0 + ) + ) ); const HighestStage = @@ -110,6 +123,51 @@ function GetAdminOverviewMissingSprites( const Missing = []; + if (Plant.DirectionalSprites === true) { + const Directions = [ + ["N", "North"], + ["E", "East"], + ["S", "South"], + ["W", "West"] + ]; + + for (const [Code, Name] of Directions) { + const Stages = new Set( + DirectionalStages + .filter( + Stage => + String( + Stage?.Direction ?? "" + ).toUpperCase() === Code + ) + .map( + Stage => + Number(Stage.Stage) + ) + .filter( + Stage => + Number.isInteger(Stage) && + Stage > 0 + ) + ); + + for ( + let Stage = 1; + Stage <= HighestStage; + Stage++ + ) { + if (!Stages.has(Stage)) { + Missing.push( + Name + "/" + + Stage + ".png" + ); + } + } + } + + return Missing; + } + for ( let Stage = 1; Stage <= HighestStage; @@ -132,7 +190,6 @@ function GetAdminOverviewMissingSprites( return Missing; } - function CollectAdminOverviewCreatedPlants() { const Created = new Set(); diff --git a/Scripts/Garden/AdminPlantEditor.js b/Scripts/Garden/AdminPlantEditor.js index 478eb65..d49fea9 100644 --- a/Scripts/Garden/AdminPlantEditor.js +++ b/Scripts/Garden/AdminPlantEditor.js @@ -168,6 +168,16 @@ function BindAdminPlantEditor() { } ); + document.getElementById( + "AdminPlantDirectionalSprites" + ).addEventListener( + "change", + () => { + RenderAdminPlantImageManager(); + RenderAdminPlantPreview(); + } + ); + document.getElementById( "AdminPlantImageUploadButton" ).addEventListener( @@ -540,6 +550,11 @@ function LoadAdminPlantIntoForm( ).checked = Plant.Shop?.ShopPlant === true; + document.getElementById( + "AdminPlantDirectionalSprites" + ).checked = + Plant.DirectionalSprites === true; + SetAdminPlantField( "AdminPlantBaseCost", Plant.Shop?.BaseCost ?? "" @@ -648,6 +663,11 @@ function DuplicateAdminPlant() { ).checked = SourcePlant.Shop?.ShopPlant === true; + document.getElementById( + "AdminPlantDirectionalSprites" + ).checked = + SourcePlant.DirectionalSprites === true; + SetAdminPlantField( "AdminPlantBaseCost", SourcePlant.Shop?.BaseCost ?? "" @@ -734,6 +754,10 @@ function StartNewAdminPlant() { "AdminPlantShopPlant" ).checked = false; + document.getElementById( + "AdminPlantDirectionalSprites" + ).checked = false; + SetAdminPlantField( "AdminPlantBaseCost", "" @@ -910,6 +934,11 @@ function GetAdminPlantFormData() { Effects: ParseAdminPlantEffects(), + DirectionalSprites: + document.getElementById( + "AdminPlantDirectionalSprites" + ).checked, + ShopPlant: ShopPlant, BaseCost: @@ -1249,6 +1278,10 @@ function GetAdminPlantPreviewDefinition() { "AdminPlantHarvestMultiplier" ).value ), + DirectionalSprites: + document.getElementById( + "AdminPlantDirectionalSprites" + ).checked, Shop: { ShopPlant: ShopPlant, BaseCost: @@ -1426,16 +1459,43 @@ function RenderAdminPlantImageManager() { "AdminPlantImageUploadButton" ); - const FileInput = + const DefaultUpload = document.getElementById( - "AdminPlantImageFiles" + "AdminPlantDefaultImageUpload" ); + const DirectionalUpload = + document.getElementById( + "AdminPlantDirectionalImageUpload" + ); + + const DirectionalToggle = + document.getElementById( + "AdminPlantDirectionalSprites" + ); + + const Inputs = [ + "AdminPlantImageFiles", + "AdminPlantImageFilesNorth", + "AdminPlantImageFilesEast", + "AdminPlantImageFilesSouth", + "AdminPlantImageFilesWest" + ] + .map( + ElementId => + document.getElementById( + ElementId + ) + ) + .filter(Boolean); + if ( List === null || Note === null || UploadButton === null || - FileInput === null + DefaultUpload === null || + DirectionalUpload === null || + DirectionalToggle === null ) { return; } @@ -1449,6 +1509,15 @@ function RenderAdminPlantImageManager() { AdminPlantEditingKey ] ?? null; + const IsDirectional = + DirectionalToggle.checked; + + DefaultUpload.hidden = + IsDirectional; + + DirectionalUpload.hidden = + !IsDirectional; + const CanEdit = Plant !== null && !AdminPlantImagePending; @@ -1456,8 +1525,9 @@ function RenderAdminPlantImageManager() { UploadButton.disabled = !CanEdit; - FileInput.disabled = - !CanEdit; + for (const Input of Inputs) { + Input.disabled = !CanEdit; + } if (Plant === null) { Note.textContent = @@ -1466,6 +1536,7 @@ function RenderAdminPlantImageManager() { } const StaticImages = + typeof PlantImages !== "undefined" && Array.isArray( PlantImages[ AdminPlantEditingKey @@ -1488,6 +1559,44 @@ function RenderAdminPlantImageManager() { ) : []; + const DirectionOrder = { + N: 0, + E: 1, + S: 2, + W: 3 + }; + + const DirectionNames = { + N: "North", + E: "East", + S: "South", + W: "West" + }; + + const DirectionalStages = + Array.isArray( + Plant.DirectionalImageStages + ) + ? [...Plant.DirectionalImageStages] + .sort( + (A, B) => + Number(A.Stage) - + Number(B.Stage) || + ( + DirectionOrder[ + String(A.Direction) + .toUpperCase() + ] ?? 99 + ) - + ( + DirectionOrder[ + String(B.Direction) + .toUpperCase() + ] ?? 99 + ) + ) + : []; + const FallbackText = StaticImages.length > 0 ? " Repository fallback: " + @@ -1500,28 +1609,45 @@ function RenderAdminPlantImageManager() { ) : ""; - if (ApiStages.length === 0) { + const DisplayStages = + IsDirectional + ? DirectionalStages + : ApiStages; + + if (DisplayStages.length === 0) { Note.textContent = - "No API sprites uploaded." + - FallbackText; + IsDirectional + ? "No directional API sprites uploaded. Choose stage-numbered PNGs for each direction." + + FallbackText + : "No API sprites uploaded." + + FallbackText; return; } Note.textContent = - ApiStages.length + - " API sprite" + + DisplayStages.length + + (IsDirectional + ? " directional API sprite" + : " API sprite") + ( - ApiStages.length === 1 + DisplayStages.length === 1 ? " uploaded." : "s uploaded." ) + FallbackText; - for (const ImageStage of ApiStages) { + for (const ImageStage of DisplayStages) { const Stage = Number( ImageStage.Stage ); + const Direction = + IsDirectional + ? String( + ImageStage.Direction ?? "" + ).toUpperCase() + : null; + const Row = document.createElement( "div" @@ -1550,13 +1676,24 @@ function RenderAdminPlantImageManager() { GetAdminPlantApiImageUrl( Plant.Id, Stage, - ImageStage.Revision + ImageStage.Revision, + Direction ); Image.alt = Plant.Name + " stage " + - Stage; + Stage + + ( + Direction === null + ? "" + : " " + + ( + DirectionNames[ + Direction + ] ?? Direction + ) + ); Preview.appendChild(Image); @@ -1566,7 +1703,17 @@ function RenderAdminPlantImageManager() { ); Label.textContent = - "Stage " + Stage; + "Stage " + Stage + + ( + Direction === null + ? "" + : " ยท " + + ( + DirectionNames[ + Direction + ] ?? Direction + ) + ); const RemoveButton = document.createElement( @@ -1586,7 +1733,8 @@ function RenderAdminPlantImageManager() { () => DeleteAdminPlantImage( Plant.Id, - Stage + Stage, + Direction ) ); @@ -1604,14 +1752,24 @@ function RenderAdminPlantImageManager() { function GetAdminPlantApiImageUrl( PlantId, Stage, - Revision = 0 + Revision = 0, + Direction = null ) { + const QueryData = { + PlantId: String(PlantId), + Stage: String(Stage), + v: String(Revision ?? 0) + }; + + if (Direction !== null) { + QueryData.Direction = + String(Direction); + } + const Query = - new URLSearchParams({ - PlantId: String(PlantId), - Stage: String(Stage), - v: String(Revision ?? 0) - }); + new URLSearchParams( + QueryData + ); return ApiUrl + "/PlantImage.php?" + @@ -1619,6 +1777,41 @@ function GetAdminPlantApiImageUrl( } +function GetAdminPlantImageUploadSets() { + const IsDirectional = + document.getElementById( + "AdminPlantDirectionalSprites" + ).checked; + + if (!IsDirectional) { + return [ + { + Direction: null, + Input: + document.getElementById( + "AdminPlantImageFiles" + ) + } + ]; + } + + return [ + ["N", "AdminPlantImageFilesNorth"], + ["E", "AdminPlantImageFilesEast"], + ["S", "AdminPlantImageFilesSouth"], + ["W", "AdminPlantImageFilesWest"] + ].map( + ([Direction, ElementId]) => ({ + Direction: Direction, + Input: + document.getElementById( + ElementId + ) + }) + ); +} + + async function UploadAdminPlantImages() { if ( AdminPlantImagePending || @@ -1636,67 +1829,61 @@ async function UploadAdminPlantImages() { return; } - const FileInput = - document.getElementById( - "AdminPlantImageFiles" - ); - - const Files = - Array.from( - FileInput.files ?? [] - ); - - if (Files.length === 0) { - SetAdminPlantMessage( - "Choose one or more PNG sprites first." - ); - return; - } - - const Stages = new Set(); const Uploads = []; try { - for (const File of Files) { - const Match = - /^(\\d+)\\.png$/i.exec( - File.name - ); - - if (Match === null) { - throw new Error( - "Sprite filenames must be stage numbers such as 1.png or 3.png." - ); - } - - const Stage = Number( - Match[1] + for ( + const UploadSet + of GetAdminPlantImageUploadSets() + ) { + const Files = Array.from( + UploadSet.Input?.files ?? [] ); - if ( - !Number.isInteger(Stage) || - Stage < 1 || - Stage > 255 - ) { - throw new Error( - "Sprite stages must be between 1 and 255." + const Stages = new Set(); + + for (const File of Files) { + const Match = + /^(\d+)\.png$/i.exec( + File.name + ); + + if (Match === null) { + throw new Error( + "Sprite filenames must be stage numbers such as 1.png or 3.png." + ); + } + + const Stage = Number( + Match[1] ); + + if ( + !Number.isInteger(Stage) || + Stage < 1 || + Stage > 255 + ) { + throw new Error( + "Sprite stages must be between 1 and 255." + ); + } + + if (Stages.has(Stage)) { + throw new Error( + "Stage " + Stage + + " was selected more than once for the same direction." + ); + } + + Stages.add(Stage); + + Uploads.push({ + Stage: Stage, + Direction: + UploadSet.Direction, + File: File + }); } - - if (Stages.has(Stage)) { - throw new Error( - "Stage " + - Stage + - " was selected more than once." - ); - } - - Stages.add(Stage); - - Uploads.push({ - Stage: Stage, - File: File - }); } } catch (Error) { SetAdminPlantMessage( @@ -1705,6 +1892,13 @@ async function UploadAdminPlantImages() { return; } + if (Uploads.length === 0) { + SetAdminPlantMessage( + "Choose one or more PNG sprites first." + ); + return; + } + AdminPlantImagePending = true; RenderAdminPlantImageManager(); @@ -1719,18 +1913,32 @@ async function UploadAdminPlantImages() { Upload.File ); + const RequestData = { + PlantId: Plant.Id, + Stage: Upload.Stage, + MimeType: "image/png", + DataBase64: DataBase64 + }; + + if (Upload.Direction !== null) { + RequestData.Direction = + Upload.Direction; + } + await AdminPlantRequest( "UploadImage", - { - PlantId: Plant.Id, - Stage: Upload.Stage, - MimeType: "image/png", - DataBase64: DataBase64 - } + RequestData ); } - FileInput.value = ""; + for ( + const UploadSet + of GetAdminPlantImageUploadSets() + ) { + if (UploadSet.Input !== null) { + UploadSet.Input.value = ""; + } + } localStorage.removeItem( "SarahtoninGardenContent" @@ -1818,7 +2026,8 @@ function ReadAdminPlantFileBase64( async function DeleteAdminPlantImage( PlantId, - Stage + Stage, + Direction = null ) { if (AdminPlantImagePending) { return; @@ -1830,16 +2039,28 @@ async function DeleteAdminPlantImage( SetAdminPlantMessage( "Removing stage " + Stage + + ( + Direction === null + ? "" + : " " + Direction + ) + " sprite..." ); try { + const RequestData = { + PlantId: PlantId, + Stage: Stage + }; + + if (Direction !== null) { + RequestData.Direction = + Direction; + } + await AdminPlantRequest( "DeleteImage", - { - PlantId: PlantId, - Stage: Stage - } + RequestData ); localStorage.removeItem( @@ -1853,6 +2074,11 @@ async function DeleteAdminPlantImage( SetAdminPlantMessage( "Stage " + Stage + + ( + Direction === null + ? "" + : " " + Direction + ) + " sprite removed." ); } catch (Error) { @@ -1888,6 +2114,8 @@ function GetAdminPlantPortableData( Plant.Effects ?? {} ) ), + DirectionalSprites: + Plant.DirectionalSprites === true, Shop: { ShopPlant: Plant.ShopPlant === true, @@ -2060,6 +2288,9 @@ async function ImportAdminPlantJson( ) ) : {}, + DirectionalSprites: + Imported.DirectionalSprites === + true, ShopPlant: Shop.ShopPlant === true || Imported.ShopPlant === true, @@ -2120,6 +2351,11 @@ async function ImportAdminPlantJson( "AdminPlantShopPlant" ).checked = Plant.ShopPlant; + document.getElementById( + "AdminPlantDirectionalSprites" + ).checked = + Plant.DirectionalSprites; + SetAdminPlantField( "AdminPlantBaseCost", Plant.BaseCost ?? "" diff --git a/Scripts/Garden/AdminValidation.js b/Scripts/Garden/AdminValidation.js index 88b512f..b6944e2 100644 --- a/Scripts/Garden/AdminValidation.js +++ b/Scripts/Garden/AdminValidation.js @@ -205,6 +205,15 @@ function ValidateAdminPlant( ); } + if ( + Plant.DirectionalSprites !== undefined && + typeof Plant.DirectionalSprites !== "boolean" + ) { + throw new Error( + "Directional sprites must be On or Off." + ); + } + if (Plant.ShopPlant === true) { ValidateAdminInteger( Number(Plant.BaseCost), diff --git a/Scripts/Garden/Garden.js b/Scripts/Garden/Garden.js index b1d386d..3dddc34 100644 --- a/Scripts/Garden/Garden.js +++ b/Scripts/Garden/Garden.js @@ -26,6 +26,20 @@ const GardenBorderColours = [ "var(--Lavender)" ]; +const GardenPlotDefaultRotation = "East"; +const GardenPlotRotations = [ + "North", + "East", + "South", + "West" +]; +const GardenPlotRotationLabels = { + North: "^", + East: "->", + South: "v", + West: "<-" +}; + function GetGardenBorderColour( GardenIndex @@ -1383,7 +1397,9 @@ function AreGardenPlotsEqual( return ( A.Plant === B.Plant && Number(A.PlantedAt) === - Number(B.PlantedAt) + Number(B.PlantedAt) && + GetGardenPlotRotation(A) === + GetGardenPlotRotation(B) ); } @@ -1405,44 +1421,135 @@ function GetGardenDisplaySettings() { 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 Button = + const Tile = + document.createElement( + "div" + ); + + Tile.className = + "PlantTile GardenPlot"; + + const MainButton = document.createElement( "button" ); - Button.className = - "PlantTile GardenPlot"; - - Button.type = "button"; + MainButton.className = + "GardenPlotMain"; + MainButton.type = "button"; if ( DisplaySettings.ShowPlantNames ) { - Button.classList.add( + Tile.classList.add( "GardenPlotWithName" ); } - if ( - DisplaySettings.ShowGrowthTimers - ) { - Button.classList.add( - "GardenPlotWithTimer" - ); - } - const Plant = Plot === null @@ -1452,7 +1559,7 @@ function CreatePlotElement( if ( DisplaySettings.ShowPlantNames ) { - Button.appendChild( + MainButton.appendChild( CreateGardenPlotName( Plot, Plant @@ -1469,43 +1576,45 @@ function CreatePlotElement( Visual.className = "GardenPlotVisual"; - Button.appendChild( + MainButton.appendChild( Visual ); + Tile.appendChild( + MainButton + ); + if (Plot === null) { - Button.classList.add( + Tile.classList.add( "GardenPlotEmpty" ); - Button.title = + MainButton.title = IsPreview ? "Future Sight: empty plot" : "Empty plot"; - if ( - DisplaySettings - .ShowGrowthTimers - ) { - Button.appendChild( - CreateGardenPlotTimer( - null, - null, - 0 - ) - ); - } + AppendGardenPlotFooter( + Tile, + null, + null, + 0, + PlotIndex, + DisplaySettings, + IsPreview + ); if (IsPreview) { - return Button; + MainButton.disabled = true; + return Tile; } if ( SelectedGardenTool === "Trowel" ) { - Button.addEventListener( + MainButton.addEventListener( "click", () => { PlantSeed( @@ -1517,40 +1626,40 @@ function CreatePlotElement( SelectedGardenTool === "MagicTrowel" ) { - Button.classList.add( + Tile.classList.add( "GardenPlotMagicPlant" ); - Button.title = + MainButton.title = "Fill empty plots with the selected seed"; - Button.addEventListener( + MainButton.addEventListener( "click", PlantManySeeds ); + } else { + MainButton.disabled = true; } - return Button; + return Tile; } if (Plant === undefined) { Visual.textContent = "?"; - if ( - DisplaySettings - .ShowGrowthTimers - ) { - Button.appendChild( - CreateGardenPlotTimer( - Plot, - null, - 0 - ) - ); - } + AppendGardenPlotFooter( + Tile, + Plot, + null, + 0, + PlotIndex, + DisplaySettings, + IsPreview + ); - return Button; + MainButton.disabled = true; + return Tile; } @@ -1560,7 +1669,7 @@ function CreatePlotElement( Plant ); - Button.style.setProperty( + Tile.style.setProperty( "--GrowthProgress", `${Progress * 100}%` ); @@ -1608,25 +1717,24 @@ function CreatePlotElement( } - if ( - DisplaySettings.ShowGrowthTimers - ) { - Button.appendChild( - CreateGardenPlotTimer( - Plot, - Plant, - Progress - ) - ); - } + AppendGardenPlotFooter( + Tile, + Plot, + Plant, + Progress, + PlotIndex, + DisplaySettings, + IsPreview + ); if (IsPreview) { - Button.title = + MainButton.title = "Future Sight: " + Plant.Name; - return Button; + MainButton.disabled = true; + return Tile; } @@ -1634,15 +1742,15 @@ function CreatePlotElement( SelectedGardenTool === "Shovel" ) { - Button.classList.add( + Tile.classList.add( "GardenPlotRemove" ); - Button.title = + MainButton.title = "Remove " + Plant.Name; - Button.addEventListener( + MainButton.addEventListener( "click", () => { RemovePlant( @@ -1651,7 +1759,7 @@ function CreatePlotElement( } ); - return Button; + return Tile; } @@ -1660,18 +1768,19 @@ function CreatePlotElement( "Fertilizer" ) { if (Progress >= 1) { - Button.title = + MainButton.title = Plant.Name + " is already fully grown"; - return Button; + MainButton.disabled = true; + return Tile; } - Button.classList.add( + Tile.classList.add( "GardenPlotFertilize" ); - Button.title = + MainButton.title = "Fertilize " + Plant.Name + " (" + @@ -1680,7 +1789,7 @@ function CreatePlotElement( ) + " minutes)"; - Button.addEventListener( + MainButton.addEventListener( "click", () => { FertilizePlant( @@ -1689,7 +1798,7 @@ function CreatePlotElement( } ); - return Button; + return Tile; } @@ -1697,7 +1806,7 @@ function CreatePlotElement( SelectedGardenTool !== "Trowel" ) { - Button.title = + MainButton.title = Plant.Name + " - " + Math.floor( @@ -1705,22 +1814,23 @@ function CreatePlotElement( ) + "% grown"; - return Button; + MainButton.disabled = true; + return Tile; } if ( Progress >= 1 ) { - Button.classList.add( + Tile.classList.add( "GardenPlotMature" ); - Button.title = + MainButton.title = "Harvest " + Plant.Name; - Button.addEventListener( + MainButton.addEventListener( "click", () => { HarvestPlant( @@ -1729,17 +1839,114 @@ function CreatePlotElement( } ); } else { - Button.title = + MainButton.title = Plant.Name + " - " + Math.floor( Progress * 100 ) + "% grown"; + + MainButton.disabled = true; } - return Button; + 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); } @@ -1946,7 +2153,8 @@ async function PlantSeed( PlotIndex ] = { Plant: PlantKey, - PlantedAt: ActionTime + PlantedAt: ActionTime, + Rotation: GardenPlotDefaultRotation }; @@ -2072,7 +2280,8 @@ async function PlantManySeeds() { EmptyPlotIndexes[Index] ] = { Plant: PlantKey, - PlantedAt: PlantedAt + PlantedAt: PlantedAt, + Rotation: GardenPlotDefaultRotation }; } @@ -2597,8 +2806,17 @@ function GetPlantImages( Plot, Plant ) { + const Direction = + Plot !== null && + Plant?.DirectionalSprites === true + ? GetGardenPlotRotation( + Plot + ) + : null; + return GetPlantImageSources( - Plant ?? Plot.Plant + Plant ?? Plot.Plant, + Direction ); } diff --git a/Scripts/Garden/MutationEngine.js b/Scripts/Garden/MutationEngine.js index aa8ef8c..b29a76d 100644 --- a/Scripts/Garden/MutationEngine.js +++ b/Scripts/Garden/MutationEngine.js @@ -768,6 +768,27 @@ function ApplyMutationResult( continue; } + const ExistingPlot = + SaveData.Garden.Plots[ + PlotIndex + ]; + + if ( + NewPlot !== null && + typeof NewPlot === "object" && + typeof NewPlot.Rotation !== + "string" + ) { + NewPlot.Rotation = + ExistingPlot !== null && + typeof ExistingPlot === + "object" && + typeof ExistingPlot.Rotation === + "string" + ? ExistingPlot.Rotation + : "East"; + } + SaveData.Garden.Plots[ PlotIndex diff --git a/Scripts/Garden/Plants.js b/Scripts/Garden/Plants.js index dbdc640..d964e09 100644 --- a/Scripts/Garden/Plants.js +++ b/Scripts/Garden/Plants.js @@ -1,5 +1,13 @@ let Plants = {}; +const PlantDirectionCodes = { + North: "N", + East: "E", + South: "S", + West: "W" +}; + + async function FetchPlantsFromApi() { const Response = @@ -50,7 +58,8 @@ async function FetchPlantsFromApi() { } function GetPlantImageSources( - PlantOrKey + PlantOrKey, + Direction = null ) { const PlantKey = typeof PlantOrKey === "string" @@ -86,10 +95,43 @@ function GetPlantImageSources( ? Plant.ImageStages : []; + const DirectionCode = + typeof Direction === "string" + ? ( + PlantDirectionCodes[ + Direction + ] ?? + Direction.toUpperCase() + ) + : null; + + const DirectionalStages = + Plant?.DirectionalSprites === true && + DirectionCode !== null && + Array.isArray( + Plant?.DirectionalImageStages + ) + ? Plant.DirectionalImageStages + .filter( + ImageStage => + String( + ImageStage?.Direction ?? + "" + ).toUpperCase() === + DirectionCode + ) + : []; + let HighestStage = StaticImages.length; - for (const ImageStage of ApiStages) { + for ( + const ImageStage + of [ + ...ApiStages, + ...DirectionalStages + ] + ) { const Stage = Number( typeof ImageStage === "object" && ImageStage !== null @@ -156,10 +198,46 @@ function GetPlantImageSources( Query.toString(); } + for ( + const ImageStage + of DirectionalStages + ) { + const Stage = Number( + ImageStage?.Stage + ); + + if ( + !Number.isInteger(Stage) || + Stage < 1 + ) { + continue; + } + + const Revision = Number( + ImageStage?.Revision ?? 0 + ); + + const Query = + new URLSearchParams({ + PlantId: String(Plant.Id), + Stage: String(Stage), + Direction: DirectionCode, + v: String( + Number.isFinite(Revision) + ? Revision + : 0 + ) + }); + + Images[Stage - 1] = + ApiUrl + + "/PlantImage.php?" + + Query.toString(); + } + return Images; } - function GetPlantMatureImageSource( PlantOrKey ) { diff --git a/Scripts/Garden/Profile.js b/Scripts/Garden/Profile.js index f11e132..9b7ee07 100644 --- a/Scripts/Garden/Profile.js +++ b/Scripts/Garden/Profile.js @@ -92,6 +92,10 @@ function BindGardenDisplayPreferences() { "ToggleGrowthTimersButton", "ShowGrowthTimers" ], + [ + "TogglePlotRotationButton", + "ShowPlotRotation" + ], [ "ToggleNextHarvestButton", "ShowNextHarvest" @@ -150,6 +154,7 @@ function BindGardenDisplayPreferences() { function RenderGardenDisplayPreferences() { RenderToolSelectionPreferences(); RenderPlantInformationPreferences(); + RenderPlotRotationPreference(); RenderGardenOverviewPreferences(); } @@ -262,6 +267,44 @@ function RenderPlantInformationPreferences() { } +function RenderPlotRotationPreference() { + const Button = + document.getElementById( + "TogglePlotRotationButton" + ); + + const Message = + document.getElementById( + "PlotRotationDisplayMessage" + ); + + if ( + Button === null || + Message === null + ) { + return; + } + + const IsOwned = + HasRotationUpgrade( + ProfileSave + ); + + SetPreferenceButtonState( + Button, + "Plot rotation", + ProfileSave.Preferences + .ShowPlotRotation !== false, + !IsOwned + ); + + Message.textContent = + IsOwned + ? "Plot directions can be hidden without changing their saved rotation." + : "Unlock Plot rotation in the Shop to use this setting."; +} + + function RenderGardenOverviewPreferences() { const Settings = [ [ @@ -378,6 +421,10 @@ async function ToggleGardenDisplayPreference( "ShowGrowthTimers" ]; + const RotationPreferences = [ + "ShowPlotRotation" + ]; + const GardenOverviewPreferences = [ "ShowNextHarvest", "ShowGardenSize", @@ -398,6 +445,11 @@ async function ToggleGardenDisplayPreference( PreferenceName ); + const IsRotationPreference = + RotationPreferences.includes( + PreferenceName + ); + const IsGardenOverviewPreference = GardenOverviewPreferences.includes( PreferenceName @@ -407,6 +459,7 @@ async function ToggleGardenDisplayPreference( if ( !IsToolSelectionPreference && !IsPlantInformationPreference && + !IsRotationPreference && !IsGardenOverviewPreference ) { return; @@ -421,6 +474,15 @@ async function ToggleGardenDisplayPreference( return; } + if ( + IsRotationPreference && + !HasRotationUpgrade( + ProfileSave + ) + ) { + return; + } + if ( IsGardenOverviewPreference && !HasGardenOverviewUpgrade( diff --git a/Scripts/Garden/Save.js b/Scripts/Garden/Save.js index 19e7486..1f62223 100644 --- a/Scripts/Garden/Save.js +++ b/Scripts/Garden/Save.js @@ -15,7 +15,7 @@ const MaximumGardenNameLength = 32; const StartingGardenWidth = 4; const StartingGardenHeight = 4; -const CurrentSaveVersion = 6; +const CurrentSaveVersion = 7; function CreateSimulationRandomState() { @@ -274,6 +274,20 @@ function NormalizeGardenData( Date.now() ); + const ValidRotations = [ + "North", + "East", + "South", + "West" + ]; + + Plot.Rotation = + ValidRotations.includes( + Plot.Rotation + ) + ? Plot.Rotation + : "East"; + delete Plot.VisualVariant; delete Plot.AddedTags; } @@ -571,7 +585,8 @@ function CreateNewSave() { GardenOverview: false, GardenEconomy: false, MutationHints: false, - MutationHintsLevel: 0 + MutationHintsLevel: 0, + Rotation: false }, Preferences: { @@ -584,7 +599,8 @@ function CreateNewSave() { ShowGrowingPlots: true, ShowReadyPlots: true, SelectQuickBoughtPlant: true, - SelectTrowelWithInventoryPlant: true + SelectTrowelWithInventoryPlant: true, + ShowPlotRotation: true }, Gardens: [ @@ -831,6 +847,9 @@ function NormalizeSaveData( SaveData.Upgrades.MutationHints === true; + SaveData.Upgrades.Rotation = + SaveData.Upgrades.Rotation === true; + SaveData.Upgrades.MutationHintsLevel = Math.max( 0, @@ -888,6 +907,10 @@ function NormalizeSaveData( SaveData.Preferences.SelectTrowelWithInventoryPlant !== false; + SaveData.Preferences.ShowPlotRotation = + SaveData.Preferences.ShowPlotRotation !== + false; + const LegacyGarden = SaveData.Garden ?? diff --git a/Scripts/Garden/Shop.js b/Scripts/Garden/Shop.js index 94b65d7..21b3c82 100644 --- a/Scripts/Garden/Shop.js +++ b/Scripts/Garden/Shop.js @@ -1114,6 +1114,7 @@ function RenderGardenUpgrades() { "Row" ), CreateNewGardenCard(), + CreateRotationUpgradeCard(), CreatePlantInformationUpgradeCard(), CreateGardenOverviewUpgradeCard(), CreateGardenEconomyUpgradeCard(), @@ -1377,6 +1378,117 @@ function CreateNewGardenCard() { } +function CreateRotationUpgradeCard() { + const Cost = + GetRotationUpgradeCost(); + + const IsOwned = + HasRotationUpgrade( + ShopSave + ); + + const Card = + document.createElement( + "article" + ); + + Card.className = + "Panel ShopItem"; + + const Header = + document.createElement( + "div" + ); + + Header.className = + "PanelHeader PanelHeaderInset ShopItemHeader" + + ( + IsOwned + ? " ShopItemHeaderOwned" + : "" + ); + + const Name = + document.createElement( + "h3" + ); + + Name.textContent = + "Plot rotation"; + + Header.appendChild(Name); + + const Description = + document.createElement( + "p" + ); + + Description.textContent = + "Rotate planted plots clockwise. Direction is stored per plot and can be shown beside the plot timer."; + + const Details = + document.createElement( + "div" + ); + + Details.className = + "ShopItemDetails"; + + Details.append( + CreateShopStat( + "Current", + IsOwned + ? "Rotation unlocked" + : "Rotation locked" + ), + CreateShopStat( + "Next", + IsOwned + ? "Purchased" + : "Rotation controls" + ) + ); + + const BuyButton = + document.createElement( + "button" + ); + + BuyButton.className = + "ActionButton ShopBuyButton"; + BuyButton.type = "button"; + + if (IsOwned) { + BuyButton.textContent = + "Purchased"; + BuyButton.disabled = true; + } else { + BuyButton.textContent = + "Buy - " + + Cost.toLocaleString() + + " Dew"; + + BuyButton.disabled = + ShopSave.Currency.Dew < Cost || + ShopPurchasePending; + + BuyButton.addEventListener( + "click", + BuyRotationUpgrade + ); + } + + Card.append( + Header, + Description, + Details, + BuyButton + ); + + return Card; +} + + function CreatePlantInformationUpgradeCard() { const Cost = GetPlantInformationUpgradeCost(); @@ -2854,6 +2966,55 @@ async function BuyNewGarden() { } +async function BuyRotationUpgrade() { + if ( + ShopPurchasePending || + HasRotationUpgrade( + ShopSave + ) + ) { + return; + } + + const Cost = + GetRotationUpgradeCost(); + + if (ShopSave.Currency.Dew < Cost) { + SetShopMessage( + "You don't have enough Dew for Plot rotation." + ); + return; + } + + ShopPurchasePending = true; + + try { + ShopSave.Currency.Dew -= Cost; + ShopSave.Statistics.CurrencySpent.Dew += + Cost; + + UnlockRotation( + ShopSave + ); + + SetShopMessage( + "Bought Plot rotation for " + + Cost.toLocaleString() + + " Dew. Rotation controls are now visible on planted plots." + ); + + RenderShop(); + + await SaveGame( + ShopSave + ); + } finally { + ShopPurchasePending = false; + RenderShop(); + } +} + + async function BuyPlantInformationUpgrade() { if ( ShopPurchasePending || diff --git a/Scripts/Garden/Upgrades.js b/Scripts/Garden/Upgrades.js index 7762e2a..54dd4b0 100644 --- a/Scripts/Garden/Upgrades.js +++ b/Scripts/Garden/Upgrades.js @@ -27,6 +27,7 @@ const MutationHintsUnlockCost = 1500; const MutationHintsUpgradeCostMultiplier = 2; const MutationHintsMaximumLevel = 1; const MutationHintsMaximumVisible = 10; +const RotationUnlockCost = 1200; const NewGardenBaseCost = 4000; const NewGardenCostMultiplier = 1.5; @@ -175,6 +176,34 @@ function AddNewGarden( } +function HasRotationUpgrade( + SaveData +) { + return ( + SaveData + ?.Upgrades + ?.Rotation === true + ); +} + + +function GetRotationUpgradeCost() { + return RotationUnlockCost; +} + + +function UnlockRotation( + SaveData +) { + SaveData.Upgrades ??= {}; + SaveData.Preferences ??= {}; + + SaveData.Upgrades.Rotation = true; + SaveData.Preferences.ShowPlotRotation = + true; +} + + function HasPlantInformationUpgrade( SaveData ) { diff --git a/Styles/Style.css b/Styles/Style.css index 635e6ec..d1d978d 100644 --- a/Styles/Style.css +++ b/Styles/Style.css @@ -634,10 +634,30 @@ input, button, textarea, select { } .GardenPlotWithName, -.GardenPlotWithTimer { +.GardenPlotWithFooter { aspect-ratio: auto; } +.GardenPlotMain { + display: grid; + width: 100%; + min-width: 0; + height: 100%; + padding: 0; + cursor: inherit; + color: inherit; + background: transparent; + border: 0; + border-radius: 0; + font: inherit; + appearance: none; + opacity: 1; +} + +.GardenPlotMain:disabled { + cursor: default; +} + .GardenPlotVisual { display: flex; width: 100%; @@ -650,7 +670,8 @@ input, button, textarea, select { } .GardenPlotName, -.GardenPlotTimer { +.GardenPlotTimer, +.GardenPlotRotationButton { display: flex; min-width: 0; min-height: 1.5rem; @@ -663,6 +684,10 @@ input, button, textarea, select { font-size: 0.75rem; line-height: 1; text-align: center; +} + +.GardenPlotName, +.GardenPlotTimer { text-overflow: ellipsis; white-space: nowrap; } @@ -671,10 +696,41 @@ input, button, textarea, select { border-bottom: 2px solid var(--Surface2); } -.GardenPlotTimer { +.GardenPlotFooter { + display: grid; + grid-template-columns: 1fr; + min-width: 0; + background-color: var(--Mantle); border-top: 2px solid var(--Surface2); } +.GardenPlotFooterSplit { + grid-template-columns: minmax(0, 1fr) auto; +} + +.GardenPlotRotationButton { + padding-inline: 0.45rem; + cursor: pointer; + border: 0; + border-radius: 0; + appearance: none; +} + +.GardenPlotFooterSplit .GardenPlotRotationButton { + border-left: 2px solid var(--Surface2); +} + +.GardenPlotRotationButton:hover { + color: var(--Sky); + background-color: var(--Surface0); +} + +.GardenPlotRotationButton:focus-visible, +.GardenPlotMain:focus-visible { + outline: 3px solid var(--Yellow); + outline-offset: -3px; +} + .GardenPlotMature .GardenPlotTimer { color: var(--Green); } @@ -686,11 +742,6 @@ input, button, textarea, select { box-shadow: 2px 2px 0 var(--Crust); } -.GardenPlot:focus-visible { - outline: 3px solid var(--Yellow); - outline-offset: 3px; -} - .GardenPlotEmpty { --TileBackground: var(--Mantle); } @@ -2304,6 +2355,17 @@ input, button, textarea, select { flex: 1 1 18rem; } +.AdminPlantDirectionalImageUpload { + display: grid; + grid-template-columns: repeat( 2, minmax(0, 1fr) ); + gap: 0.75rem; + margin-top: 0.75rem; +} + +.AdminPlantDirectionalImageUpload .AdminEditorField { + margin: 0; +} + .AdminPlantImageNote { color: var(--Overlay2); } @@ -2350,6 +2412,10 @@ input, button, textarea, select { grid-template-columns: auto minmax(0, 1fr); } + .AdminPlantDirectionalImageUpload { + grid-template-columns: 1fr; + } + .AdminPlantImageRemoveButton { grid-column: 1 / -1; width: 100%;