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;
+}