Files
srhtnin.github.io/Scripts/Garden/MutationRecipePreview.js
T

755 lines
15 KiB
JavaScript

const MutationRecipePreviewCycleMilliseconds = 1200;
let MutationRecipePreviewFrame = 0;
let MutationRecipePreviewTimer = null;
const MutationRecipePreviewCells = new Set();
const MutationRecipePreviewPlantStageCells = new Set();
const MutationRecipePreviewPermutations = new Map();
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",
CyclePlantStages = false
) {
const Entry = {
Element: Element,
List: List,
ListNumber: ListNumber,
Offset:
Number.isInteger(Offset)
? Offset
: 0,
PlantCatalogue:
PlantCatalogue ?? {},
TextClassName: TextClassName,
CyclePlantStages:
CyclePlantStages === true
};
MutationRecipePreviewCells.add(
Entry
);
RenderMutationRecipePreviewListCell(
Entry,
MutationRecipePreviewFrame
);
StartMutationRecipePreviewTimer();
}
function RegisterMutationRecipePreviewPlantStageCell(
Element,
PlantOrKey
) {
const Images =
GetMutationRecipePreviewPlantStageImages(
PlantOrKey
);
if (Images.length <= 1) {
return;
}
const Entry = {
Element: Element,
Images: Images
};
MutationRecipePreviewPlantStageCells.add(
Entry
);
RenderMutationRecipePreviewPlantStageCell(
Entry,
MutationRecipePreviewFrame
);
StartMutationRecipePreviewTimer();
}
function StartMutationRecipePreviewTimer() {
if (
MutationRecipePreviewTimer !== null ||
(
MutationRecipePreviewCells.size === 0 &&
MutationRecipePreviewPlantStageCells.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
);
}
for (
const Entry
of Array.from(
MutationRecipePreviewPlantStageCells
)
) {
if (!Entry.Element.isConnected) {
MutationRecipePreviewPlantStageCells.delete(
Entry
);
continue;
}
RenderMutationRecipePreviewPlantStageCell(
Entry,
MutationRecipePreviewFrame
);
}
if (
MutationRecipePreviewCells.size === 0 &&
MutationRecipePreviewPlantStageCells.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 =
GetMutationRecipePreviewItemIndex(
Entry,
Frame,
Items.length
);
ApplyMutationRecipePreviewCellDisplay(
Entry,
GetMutationRecipePreviewItemDisplay(
Items[ItemIndex],
Entry.PlantCatalogue,
Entry.CyclePlantStages,
Frame
)
);
}
function GetMutationRecipePreviewItemIndex(
Entry,
Frame,
ItemCount
) {
if (Entry.List?.Mode !== "Once") {
return (
Frame +
Entry.Offset
) % ItemCount;
}
const Permutation =
CreateMutationRecipePreviewPermutation(
ItemCount,
Entry.ListNumber,
Frame
);
return Permutation[
Entry.Offset % ItemCount
];
}
function CreateMutationRecipePreviewPermutation(
ItemCount,
ListNumber,
Frame
) {
const CacheKey =
ListNumber + ":" + ItemCount;
const Cached =
MutationRecipePreviewPermutations.get(
CacheKey
);
if (Cached?.Frame === Frame) {
return Cached.Permutation;
}
const Permutation =
Array.from(
{ length: ItemCount },
(_, Index) => Index
);
if (ItemCount >= 2 && Frame !== 0) {
let State =
(
Math.imul(
Frame + 1,
0x9e3779b1
) ^
Math.imul(
ListNumber + 1,
0x85ebca6b
) ^
Math.imul(
ItemCount,
0xc2b2ae35
)
) >>> 0;
if (State === 0) {
State = 0x6d2b79f5;
}
for (
let Index = ItemCount - 1;
Index > 0;
Index--
) {
State ^= State << 13;
State ^= State >>> 17;
State ^= State << 5;
State >>>= 0;
const SwapIndex =
State % (Index + 1);
[
Permutation[Index],
Permutation[SwapIndex]
] = [
Permutation[SwapIndex],
Permutation[Index]
];
}
}
if (
ItemCount >= 2 &&
Cached?.Frame === Frame - 1 &&
Permutation.every(
(Value, Index) =>
Value ===
Cached.Permutation[Index]
)
) {
[
Permutation[0],
Permutation[1]
] = [
Permutation[1],
Permutation[0]
];
}
MutationRecipePreviewPermutations.set(
CacheKey,
{
Frame: Frame,
Permutation: Permutation
}
);
return Permutation;
}
function GetMutationRecipePreviewItemDisplay(
Item,
PlantCatalogue,
CyclePlantStages = false,
Frame = 0
) {
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:
GetMutationRecipePreviewPlantImageSource(
Plant,
CyclePlantStages,
Frame
),
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 GetMutationRecipePreviewPlantStageImages(
PlantOrKey
) {
return GetPlantImageSources(
PlantOrKey
).filter(
Image =>
typeof Image === "string" &&
Image.length > 0
);
}
function GetMutationRecipePreviewPlantImageSource(
PlantOrKey,
CyclePlantStages,
Frame
) {
if (CyclePlantStages !== true) {
return GetPlantMatureImageSource(
PlantOrKey
);
}
const Images =
GetMutationRecipePreviewPlantStageImages(
PlantOrKey
);
if (Images.length === 0) {
return null;
}
return Images[
Frame % Images.length
];
}
function RenderMutationRecipePreviewPlantStageCell(
Entry,
Frame
) {
const Image =
Entry.Element.querySelector(
"img.GuideRecipeImage"
);
if (Image === null) {
return;
}
Image.src =
Entry.Images[
Frame % Entry.Images.length
];
}
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;
}