From 495e1a230a80d236fba3254e5a55474fc5e61d1c Mon Sep 17 00:00:00 2001 From: Sarahtonin Date: Mon, 7 Sep 2026 15:53:37 +0200 Subject: [PATCH] Added neat preview to plots for mutations using lists. --- Pages/Admin.php | 1 + Pages/AdminMutations.php | 1 + Pages/Mutations.php | 1 + Scripts/Garden/AdminContentTransfer.js | 55 ++- Scripts/Garden/AdminMutationEditor.js | 39 +- Scripts/Garden/MutationEncyclopedia.js | 104 ++++- Scripts/Garden/MutationRecipePreview.js | 507 ++++++++++++++++++++++++ 7 files changed, 680 insertions(+), 28 deletions(-) create mode 100644 Scripts/Garden/MutationRecipePreview.js diff --git a/Pages/Admin.php b/Pages/Admin.php index 1393875..a80235b 100644 --- a/Pages/Admin.php +++ b/Pages/Admin.php @@ -270,6 +270,7 @@ $PageSection = "Admin"; + diff --git a/Pages/AdminMutations.php b/Pages/AdminMutations.php index 4d6b5a9..8e3c3ee 100644 --- a/Pages/AdminMutations.php +++ b/Pages/AdminMutations.php @@ -555,6 +555,7 @@ $PageSection = "Admin"; + diff --git a/Pages/Mutations.php b/Pages/Mutations.php index f915608..65b6614 100644 --- a/Pages/Mutations.php +++ b/Pages/Mutations.php @@ -67,6 +67,7 @@ $PageSection = "Mutations"; + diff --git a/Scripts/Garden/AdminContentTransfer.js b/Scripts/Garden/AdminContentTransfer.js index 6adc40a..a50d424 100644 --- a/Scripts/Garden/AdminContentTransfer.js +++ b/Scripts/Garden/AdminContentTransfer.js @@ -2604,6 +2604,12 @@ function CreateAdminTransferMutationRecipe( "Keep" ); + const ListOffsets = + CreateMutationRecipePreviewOffsets( + Pattern, + Success + ); + const Flow = document.createElement( "div" @@ -2630,14 +2636,18 @@ function CreateAdminTransferMutationRecipe( "Arrange", Pattern, "Pattern", - PlantCatalogue + PlantCatalogue, + Mutation.Lists, + ListOffsets.Pattern ), Arrow, CreateAdminTransferMutationRecipePanel( "Result", Success, "Result", - PlantCatalogue + PlantCatalogue, + Mutation.Lists, + ListOffsets.Result ) ); @@ -2750,7 +2760,9 @@ function CreateAdminTransferMutationRecipePanel( Heading, Matrix, Mode, - PlantCatalogue + PlantCatalogue, + Lists, + ListOffsets ) { const Panel = document.createElement( @@ -2781,15 +2793,42 @@ function CreateAdminTransferMutationRecipePanel( ) ); - for (const Row of Matrix) { - for (const Value of Row) { - Grid.appendChild( + for ( + let Y = 0; + Y < Matrix.length; + Y++ + ) { + for ( + let X = 0; + X < Matrix[Y].length; + X++ + ) { + const Value = + Matrix[Y][X]; + + const Cell = CreateAdminTransferMutationCell( Value, Mode, PlantCatalogue - ) - ); + ); + + const ListNumber = + GetMutationRecipePreviewListReference( + Value + ); + + if (ListNumber !== null) { + RegisterMutationRecipePreviewListCell( + Cell, + Lists?.[ListNumber - 1], + ListNumber, + ListOffsets?.[Y]?.[X], + PlantCatalogue + ); + } + + Grid.appendChild(Cell); } } diff --git a/Scripts/Garden/AdminMutationEditor.js b/Scripts/Garden/AdminMutationEditor.js index e5bc21e..cf1261b 100644 --- a/Scripts/Garden/AdminMutationEditor.js +++ b/Scripts/Garden/AdminMutationEditor.js @@ -2032,16 +2032,24 @@ function ResizeAdminMutationRecipe() { function RenderAdminMutationGrids() { + const ListOffsets = + CreateMutationRecipePreviewOffsets( + AdminMutationPattern, + AdminMutationResult + ); + RenderAdminMutationGrid( "AdminMutationPatternGrid", "Pattern", - AdminMutationPattern + AdminMutationPattern, + ListOffsets.Pattern ); RenderAdminMutationGrid( "AdminMutationResultGrid", "Result", - AdminMutationResult + AdminMutationResult, + ListOffsets.Result ); } @@ -2049,7 +2057,8 @@ function RenderAdminMutationGrids() { function RenderAdminMutationGrid( ContainerId, Mode, - Matrix + Matrix, + ListOffsets ) { const Grid = document.getElementById( @@ -2080,7 +2089,8 @@ function RenderAdminMutationGrid( Mode, X, Y, - Matrix[Y][X] + Matrix[Y][X], + ListOffsets?.[Y]?.[X] ) ); } @@ -2092,7 +2102,8 @@ function CreateAdminMutationGridCell( Mode, X, Y, - Value + Value, + ListOffset ) { const Button = document.createElement( @@ -2167,6 +2178,24 @@ function CreateAdminMutationGridCell( Button.title = Display.Label; + const ListNumber = + GetMutationRecipePreviewListReference( + Value + ); + + if (ListNumber !== null) { + RegisterMutationRecipePreviewListCell( + Button, + AdminMutationLists[ + ListNumber - 1 + ], + ListNumber, + ListOffset, + AdminMutationPlantCatalogue, + "GuideRecipeLabel" + ); + } + Button.addEventListener( "click", () => { diff --git a/Scripts/Garden/MutationEncyclopedia.js b/Scripts/Garden/MutationEncyclopedia.js index fdc4ea1..dbd3a78 100644 --- a/Scripts/Garden/MutationEncyclopedia.js +++ b/Scripts/Garden/MutationEncyclopedia.js @@ -1130,6 +1130,12 @@ function CreateMutationRecipeFlow( "Keep" ); + const ListOffsets = + CreateMutationRecipePreviewOffsets( + Pattern, + Success + ); + const Flow = document.createElement( @@ -1144,7 +1150,9 @@ function CreateMutationRecipeFlow( CreateMutationRecipePanel( "Arrange", Pattern, - "Pattern" + "Pattern", + Mutation.Lists, + ListOffsets.Pattern ); const Arrow = @@ -1168,7 +1176,9 @@ function CreateMutationRecipeFlow( CreateMutationRecipePanel( "Result", Success, - "Result" + "Result", + Mutation.Lists, + ListOffsets.Result ); @@ -1288,7 +1298,9 @@ function GetMutationRecipeListItemLabel( function CreateMutationRecipePanel( Heading, Matrix, - Mode + Mode, + Lists, + ListOffsets ) { const Panel = document.createElement( @@ -1328,17 +1340,24 @@ function CreateMutationRecipePanel( for ( - const Row - of Matrix + let Y = 0; + Y < Matrix.length; + Y++ ) { for ( - const Value - of Row + let X = 0; + X < Matrix[Y].length; + X++ ) { + const Value = + Matrix[Y][X]; + const Cell = CreateMutationRecipeCell( Value, - Mode + Mode, + Lists, + ListOffsets?.[Y]?.[X] ); Grid.appendChild( @@ -1372,22 +1391,30 @@ function CreateMutationRecipePanel( function CreateMutationRecipeCell( Value, - Mode + Mode, + Lists, + ListOffset ) { if (Mode === "Result") { return CreateMutationResultCell( - Value + Value, + Lists, + ListOffset ); } return CreateMutationPatternCell( - Value + Value, + Lists, + ListOffset ); } function CreateMutationPatternCell( - Matcher + Matcher, + Lists, + ListOffset ) { if ( Matcher === null || @@ -1418,10 +1445,21 @@ function CreateMutationPatternCell( ); if (ListNumber !== null) { - return CreateMutationTextCell( + const Cell = + CreateMutationTextCell( "List " + ListNumber, "MutationRecipeMatcher" ); + + RegisterMutationRecipePreviewListCell( + Cell.Element, + Lists?.[ListNumber - 1], + ListNumber, + ListOffset, + Plants + ); + + return Cell; } return CreateMutationPlantCell( @@ -1517,7 +1555,9 @@ function CreateMutationPatternCell( function CreateMutationResultCell( - Result + Result, + Lists, + ListOffset ) { if ( Result === null || @@ -1559,10 +1599,21 @@ function CreateMutationResultCell( ); if (ListNumber !== null) { - return CreateMutationTextCell( + const Cell = + CreateMutationTextCell( "List " + ListNumber, "MutationRecipeMatcher" ); + + RegisterMutationRecipePreviewListCell( + Cell.Element, + Lists?.[ListNumber - 1], + ListNumber, + ListOffset, + Plants + ); + + return Cell; } return CreateMutationPlantCell( @@ -1589,6 +1640,29 @@ function CreateMutationResultCell( ); } + const ListNumber = + GetMutationDisplayListReference( + Result.Plant + ); + + if (ListNumber !== null) { + const Cell = + CreateMutationTextCell( + "List " + ListNumber, + "MutationRecipeMatcher" + ); + + RegisterMutationRecipePreviewListCell( + Cell.Element, + Lists?.[ListNumber - 1], + ListNumber, + ListOffset, + Plants + ); + + return Cell; + } + return CreateMutationPlantCell( Result.Plant ); diff --git a/Scripts/Garden/MutationRecipePreview.js b/Scripts/Garden/MutationRecipePreview.js new file mode 100644 index 0000000..dacaf6d --- /dev/null +++ b/Scripts/Garden/MutationRecipePreview.js @@ -0,0 +1,507 @@ +const MutationRecipePreviewCycleMilliseconds = 1200; + +let MutationRecipePreviewFrame = 0; +let MutationRecipePreviewTimer = null; + +const MutationRecipePreviewCells = new Set(); + + +function GetMutationRecipePreviewListReference( + Value +) { + let Reference = Value; + + if ( + Value !== null && + typeof Value === "object" && + !Array.isArray(Value) && + typeof Value.Plant === "string" + ) { + Reference = Value.Plant; + } + + if (typeof Reference !== "string") { + return null; + } + + const Match = + /^List:([1-9][0-9]*)$/.exec( + Reference + ); + + return Match === null + ? null + : Number(Match[1]); +} + + +function CreateMutationRecipePreviewOffsets( + Pattern, + Result +) { + const PatternOffsets = + CreateMutationRecipePreviewOffsetMatrix( + Pattern + ); + + const ResultOffsets = + Array.isArray(Result) + ? Result.map( + Row => + Array.isArray(Row) + ? Row.map(() => null) + : [] + ) + : []; + + const PatternCounts = {}; + + for ( + let Y = 0; + Y < PatternOffsets.length; + Y++ + ) { + for ( + let X = 0; + X < PatternOffsets[Y].length; + X++ + ) { + const Offset = + PatternOffsets[Y][X]; + + if (Offset === null) { + continue; + } + + const ListNumber = + GetMutationRecipePreviewListReference( + Pattern?.[Y]?.[X] + ); + + if (ListNumber === null) { + continue; + } + + PatternCounts[ListNumber] = + Math.max( + PatternCounts[ListNumber] ?? 0, + Offset + 1 + ); + } + } + + const UsedResultOffsets = {}; + + for ( + let Y = 0; + Y < ResultOffsets.length; + Y++ + ) { + for ( + let X = 0; + X < ResultOffsets[Y].length; + X++ + ) { + const ListNumber = + GetMutationRecipePreviewListReference( + Result?.[Y]?.[X] + ); + + if (ListNumber === null) { + continue; + } + + const PatternListNumber = + GetMutationRecipePreviewListReference( + Pattern?.[Y]?.[X] + ); + + const PatternOffset = + PatternOffsets?.[Y]?.[X]; + + if ( + PatternListNumber !== ListNumber || + PatternOffset === null || + PatternOffset === undefined + ) { + continue; + } + + ResultOffsets[Y][X] = + PatternOffset; + + if ( + UsedResultOffsets[ListNumber] === + undefined + ) { + UsedResultOffsets[ListNumber] = + new Set(); + } + + UsedResultOffsets[ListNumber].add( + PatternOffset + ); + } + } + + const NextOverflowOffset = {}; + + for ( + let Y = 0; + Y < ResultOffsets.length; + Y++ + ) { + for ( + let X = 0; + X < ResultOffsets[Y].length; + X++ + ) { + if (ResultOffsets[Y][X] !== null) { + continue; + } + + const ListNumber = + GetMutationRecipePreviewListReference( + Result?.[Y]?.[X] + ); + + if (ListNumber === null) { + continue; + } + + if ( + UsedResultOffsets[ListNumber] === + undefined + ) { + UsedResultOffsets[ListNumber] = + new Set(); + } + + const Used = + UsedResultOffsets[ListNumber]; + + const PatternCount = + PatternCounts[ListNumber] ?? 0; + + let Offset = 0; + + while ( + Offset < PatternCount && + Used.has(Offset) + ) { + Offset++; + } + + if (Offset >= PatternCount) { + Offset = + NextOverflowOffset[ + ListNumber + ] ?? PatternCount; + + NextOverflowOffset[ListNumber] = + Offset + 1; + } + + Used.add(Offset); + ResultOffsets[Y][X] = Offset; + } + } + + return { + Pattern: PatternOffsets, + Result: ResultOffsets + }; +} + + +function CreateMutationRecipePreviewOffsetMatrix( + Matrix +) { + if (!Array.isArray(Matrix)) { + return []; + } + + const NextOffsets = {}; + + return Matrix.map( + Row => { + if (!Array.isArray(Row)) { + return []; + } + + return Row.map( + Value => { + const ListNumber = + GetMutationRecipePreviewListReference( + Value + ); + + if (ListNumber === null) { + return null; + } + + const Offset = + NextOffsets[ListNumber] ?? 0; + + NextOffsets[ListNumber] = + Offset + 1; + + return Offset; + } + ); + } + ); +} + + +function RegisterMutationRecipePreviewListCell( + Element, + List, + ListNumber, + Offset, + PlantCatalogue, + TextClassName = "MutationRecipeText" +) { + const Entry = { + Element: Element, + List: List, + ListNumber: ListNumber, + Offset: + Number.isInteger(Offset) + ? Offset + : 0, + PlantCatalogue: + PlantCatalogue ?? {}, + TextClassName: TextClassName + }; + + MutationRecipePreviewCells.add( + Entry + ); + + RenderMutationRecipePreviewListCell( + Entry, + MutationRecipePreviewFrame + ); + + StartMutationRecipePreviewTimer(); +} + + +function StartMutationRecipePreviewTimer() { + if ( + MutationRecipePreviewTimer !== null || + MutationRecipePreviewCells.size === 0 + ) { + return; + } + + if ( + window.matchMedia?.( + "(prefers-reduced-motion: reduce)" + ).matches === true + ) { + return; + } + + MutationRecipePreviewTimer = + window.setInterval( + AdvanceMutationRecipePreviewFrame, + MutationRecipePreviewCycleMilliseconds + ); +} + + +function AdvanceMutationRecipePreviewFrame() { + MutationRecipePreviewFrame++; + + for ( + const Entry + of Array.from( + MutationRecipePreviewCells + ) + ) { + if (!Entry.Element.isConnected) { + MutationRecipePreviewCells.delete( + Entry + ); + continue; + } + + RenderMutationRecipePreviewListCell( + Entry, + MutationRecipePreviewFrame + ); + } + + if ( + MutationRecipePreviewCells.size === 0 && + MutationRecipePreviewTimer !== null + ) { + window.clearInterval( + MutationRecipePreviewTimer + ); + + MutationRecipePreviewTimer = null; + } +} + + +function RenderMutationRecipePreviewListCell( + Entry, + Frame +) { + const Items = + Array.isArray(Entry.List?.Items) + ? Entry.List.Items + : []; + + if (Items.length === 0) { + ApplyMutationRecipePreviewCellDisplay( + Entry, + { + Label: + "List " + + Entry.ListNumber, + Image: null, + ClassName: + "MutationRecipeMatcher" + } + ); + + return; + } + + const ItemIndex = + ( + Frame + + Entry.Offset + ) % Items.length; + + ApplyMutationRecipePreviewCellDisplay( + Entry, + GetMutationRecipePreviewItemDisplay( + Items[ItemIndex], + Entry.PlantCatalogue + ) + ); +} + + +function GetMutationRecipePreviewItemDisplay( + Item, + PlantCatalogue +) { + if (Item?.Type === "Plant") { + const Plant = + PlantCatalogue?.[ + Item.Value + ]; + + if (Plant === undefined) { + return { + Label: + Item.Value ?? + "Unknown plant", + Image: null, + ClassName: + "MutationRecipeMatcher" + }; + } + + return { + Label: + Plant.Name ?? + Item.Value, + Image: + GetPlantMatureImageSource( + Plant + ), + ClassName: + "GuideRecipePlant" + }; + } + + if (Item?.Type === "Tag") { + return { + Label: + Item.Value ?? "", + Image: null, + ClassName: + "MutationRecipeMatcher" + }; + } + + if (Item?.Type === "Empty") { + return { + Label: "Empty", + Image: null, + ClassName: + "GuideRecipeEmpty" + }; + } + + return { + Label: "Any", + Image: null, + ClassName: + "MutationRecipeAny" + }; +} + + +function ApplyMutationRecipePreviewCellDisplay( + Entry, + Display +) { + const Element = Entry.Element; + + Element.classList.remove( + "GuideRecipePlant", + "GuideRecipeEmpty", + "MutationRecipeAny", + "MutationRecipeMatcher" + ); + + Element.classList.add( + Display.ClassName + ); + + Element.replaceChildren(); + + if (Display.Image !== null) { + const Image = + document.createElement( + "img" + ); + + Image.className = + "PlantSprite GuideRecipeImage"; + + Image.src = Display.Image; + Image.alt = ""; + + Element.appendChild(Image); + } + + const Label = + document.createElement( + "span" + ); + + Label.className = + Display.Image === null + ? Entry.TextClassName + : "GuideRecipeLabel"; + + Label.textContent = + Display.Label; + + Element.appendChild(Label); + + Element.title = + "List " + + Entry.ListNumber + + ": " + + Display.Label; +}