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