const MaxMutationPasses = 100; function CheckGardenMutations( SaveData, AtTime = GetSimulationTime( SaveData ) ) { SaveData.MutationCooldowns ??= {}; const Result = { Attempted: false, Changed: false, SuccessfulMutations: [], NewlyDiscoveredMutations: [], NewlyDiscoveredPlants: [] }; for ( let Pass = 0; Pass < MaxMutationPasses; Pass++ ) { const Candidates = FindGardenMutationCandidates( SaveData, AtTime ) .filter( Candidate => !IsMutationCandidateOnCooldown( SaveData, Candidate, AtTime ) ) .sort( CompareMutationCandidates ); if (Candidates.length === 0) { break; } const ReservedCells = new Set(); const AcceptedCandidates = []; for ( const Candidate of Candidates ) { if ( Candidate.Cells.some( Cell => ReservedCells.has( Cell ) ) ) { continue; } AcceptedCandidates.push( Candidate ); for ( const Cell of Candidate.Cells ) { ReservedCells.add( Cell ); } } if ( AcceptedCandidates.length === 0 ) { break; } let PassChanged = false; for ( const Candidate of AcceptedCandidates ) { if ( !DoesMutationCandidateStillMatch( SaveData, Candidate, AtTime ) ) { continue; } if ( IsMutationCandidateOnCooldown( SaveData, Candidate, AtTime ) ) { continue; } Result.Attempted = true; StartMutationCandidateCooldown( SaveData, Candidate, AtTime ); const BaseChance = Math.max( 0, Math.min( Number( Candidate.Mutation.Chance ?? 1 ), 1 ) ); const Catalyst = typeof GetStrongestFunctionalEffectAffectingAnyPlot === "function" ? GetStrongestFunctionalEffectAffectingAnyPlot( SaveData, SaveData.ActiveGardenIndex ?? 0, Candidate.Cells, "MutationChanceBonus", AtTime ) : null; const CatalystBonus = typeof GetFunctionalPercentageAmount === "function" ? GetFunctionalPercentageAmount( Catalyst?.Effect ) : 0; const Chance = Math.min( 1, BaseChance + CatalystBonus ); const Succeeded = GetSimulationRandom( SaveData ) < Chance; if (Succeeded) { const NewlyDiscovered = DiscoverMutationResult( SaveData, Candidate.Mutation ); const Changed = ApplyMutationResult( SaveData, Candidate, Candidate.Success, AtTime ); Result.SuccessfulMutations.push( Candidate.Mutation.Id ); Result.NewlyDiscoveredMutations.push( ...NewlyDiscovered.Mutations ); Result.NewlyDiscoveredPlants.push( ...NewlyDiscovered.Plants ); if (Changed) { Result.Changed = true; PassChanged = true; } } else { const Changed = ApplyMutationFailure( SaveData, Candidate, AtTime ); if (Changed) { Result.Changed = true; PassChanged = true; } } } /* * Only rescan immediately if the board * actually changed. Failed attempts still * receive their cooldown, but do not cause * lower-priority overlapping recipes to be * attempted immediately afterwards. */ if (!PassChanged) { break; } } Result.SuccessfulMutations = [ ...new Set( Result.SuccessfulMutations ) ]; Result.NewlyDiscoveredMutations = [ ...new Set( Result.NewlyDiscoveredMutations ) ]; Result.NewlyDiscoveredPlants = [ ...new Set( Result.NewlyDiscoveredPlants ) ]; return Result; } function FindGardenMutationCandidates( SaveData, AtTime = GetSimulationTime( SaveData ) ) { const Candidates = []; for ( const Mutation of Object.values( MutationSets ) ) { if (Mutation.Archived === true) { continue; } for ( const Orientation of GetMutationOrientations( Mutation ) ) { const PatternHeight = Orientation.Pattern.length; const PatternWidth = Orientation.Pattern[0] ?.length ?? 0; if ( PatternWidth === 0 || PatternHeight === 0 || PatternWidth > SaveData.Garden.Width || PatternHeight > SaveData.Garden.Height ) { continue; } for ( let OriginY = 0; OriginY <= SaveData.Garden.Height - PatternHeight; OriginY++ ) { for ( let OriginX = 0; OriginX <= SaveData.Garden.Width - PatternWidth; OriginX++ ) { const Candidate = CreateMutationCandidate( SaveData, Mutation, Orientation, OriginX, OriginY, AtTime ); if (Candidate !== null) { Candidates.push( Candidate ); } } } } } return Candidates; } function CreateMutationCandidate( SaveData, Mutation, Orientation, OriginX, OriginY, AtTime = GetSimulationTime( SaveData ) ) { const Captures = {}; const Cells = []; const ListReferences = {}; for ( let LocalY = 0; LocalY < Orientation.Pattern.length; LocalY++ ) { for ( let LocalX = 0; LocalX < Orientation.Pattern[ LocalY ].length; LocalX++ ) { const PlotIndex = GetGardenPlotIndex( SaveData, OriginX + LocalX, OriginY + LocalY ); const Matcher = Orientation.Pattern[ LocalY ][ LocalX ]; const Plot = SaveData.Garden.Plots[ PlotIndex ]; Cells.push( PlotIndex ); const ListNumber = GetMutationListReference( Matcher ); if (ListNumber !== null) { ListReferences[ListNumber] ??= []; ListReferences[ListNumber].push({ PlotIndex: PlotIndex, LocalX: LocalX, LocalY: LocalY, Plot: Plot }); continue; } if ( !DoesPlotMatchMutationMatcher( Plot, Matcher, Mutation, Captures, AtTime ) ) { return null; } } } const ListMatches = MatchMutationLists( ListReferences, Mutation, AtTime ); if (ListMatches === null) { return null; } const GardenIndex = Math.max( 0, Number( SaveData.ActiveGardenIndex ?? 0 ) || 0 ); if ( typeof IsGardenPlotMutationDisabled === "function" && Cells.some( PlotIndex => IsGardenPlotMutationDisabled( SaveData, GardenIndex, PlotIndex, AtTime ) ) && !( typeof DoesMutationPatternInvolveDisableMutation === "function" && DoesMutationPatternInvolveDisableMutation( SaveData, GardenIndex, OriginX, OriginY, Orientation.Pattern ) ) ) { return null; } return { Mutation: Mutation, Pattern: Orientation.Pattern, Success: Orientation.Success, Rotation: Orientation.Rotation, OriginX: OriginX, OriginY: OriginY, Cells: Cells, Captures: Captures, ListMatches: ListMatches, Priority: Number( Mutation.Priority ?? 0 ), Specificity: GetMutationPatternSpecificity( Orientation.Pattern, Mutation ) }; } function DoesMutationCandidateStillMatch( SaveData, Candidate, AtTime = GetSimulationTime( SaveData ) ) { const RefreshedCandidate = CreateMutationCandidate( SaveData, Candidate.Mutation, { Pattern: Candidate.Pattern, Success: Candidate.Success, Rotation: Candidate.Rotation }, Candidate.OriginX, Candidate.OriginY, AtTime ); if (RefreshedCandidate === null) { return false; } Candidate.Captures = RefreshedCandidate.Captures; Candidate.ListMatches = RefreshedCandidate.ListMatches; return true; } function GetMutationListReference( Value ) { if (typeof Value !== "string") { return null; } const Match = /^List:([1-9][0-9]*)$/.exec( Value ); return Match === null ? null : Number(Match[1]); } function MatchMutationLists( ListReferences, Mutation, AtTime ) { const Matches = {}; const Lists = Array.isArray(Mutation.Lists) ? Mutation.Lists : []; for ( const [ListNumberText, References] of Object.entries(ListReferences) ) { const ListNumber = Number(ListNumberText); const List = Lists[ListNumber - 1]; if ( List === undefined || !Array.isArray(List.Items) || List.Items.length === 0 ) { return null; } const CandidateItems = References.map( Reference => List.Items.map( (Item, ItemIndex) => ({ Item: Item, ItemIndex: ItemIndex }) ).filter( Candidate => DoesPlotMatchMutationListItem( Reference.Plot, Candidate.Item, Mutation, AtTime ) ) ); if ( CandidateItems.some( Items => Items.length === 0 ) ) { return null; } let Assignments; if (List.Mode === "Once") { Assignments = MatchMutationListOnce( CandidateItems ); if (Assignments === null) { return null; } } else { Assignments = CandidateItems.map( Items => Items[0] ); } Matches[ListNumber] = References.map( (Reference, Index) => ({ PlotIndex: Reference.PlotIndex, LocalX: Reference.LocalX, LocalY: Reference.LocalY, Plot: CloneMutationPlot( Reference.Plot ), ItemIndex: Assignments[Index] .ItemIndex }) ); } return Matches; } function MatchMutationListOnce( CandidateItems ) { const ItemToReference = new Map(); const Assignments = new Array( CandidateItems.length ); function TryAssign( ReferenceIndex, SeenItems ) { for ( const Candidate of CandidateItems[ ReferenceIndex ] ) { if ( SeenItems.has( Candidate.ItemIndex ) ) { continue; } SeenItems.add( Candidate.ItemIndex ); const PreviousReference = ItemToReference.get( Candidate.ItemIndex ); if ( PreviousReference === undefined || TryAssign( PreviousReference, SeenItems ) ) { ItemToReference.set( Candidate.ItemIndex, ReferenceIndex ); Assignments[ ReferenceIndex ] = Candidate; return true; } } return false; } for ( let ReferenceIndex = 0; ReferenceIndex < CandidateItems.length; ReferenceIndex++ ) { if ( !TryAssign( ReferenceIndex, new Set() ) ) { return null; } } return Assignments; } function DoesPlotMatchMutationListItem( Plot, Item, Mutation, AtTime ) { if ( Item === null || typeof Item !== "object" ) { return false; } if (Item.Type === "Any") { return true; } if (Item.Type === "Empty") { return Plot === null; } if (Plot === null) { return false; } const Plant = Plants[Plot.Plant]; if (Plant === undefined) { return false; } if ( Mutation.AllowImmature !== true && !IsMutationPlantMature( Plot, Plant, AtTime ) ) { return false; } if (Item.Type === "Plant") { return ( typeof Item.Value === "string" && Plot.Plant === Item.Value ); } if (Item.Type === "Tag") { return ( typeof Item.Value === "string" && Array.isArray(Plant.Tags) && Plant.Tags.includes( Item.Value ) ); } return false; } function DoesPlotMatchMutationMatcher( Plot, Matcher, Mutation, Captures, AtTime ) { if ( Matcher === null || Matcher === "Any" ) { return true; } if (Matcher === "Empty") { return Plot === null; } if (Plot === null) { return false; } const Plant = Plants[Plot.Plant]; if (Plant === undefined) { return false; } if ( Mutation.AllowImmature !== true && !IsMutationPlantMature( Plot, Plant, AtTime ) ) { return false; } if ( typeof Matcher === "string" ) { return Plot.Plant === Matcher; } if ( typeof Matcher !== "object" ) { return false; } if ( typeof Matcher.Plant === "string" && Plot.Plant !== Matcher.Plant ) { return false; } const Tags = Plant.Tags ?? []; if ( Array.isArray( Matcher.Tags ) && !Matcher.Tags.every( Tag => Tags.includes(Tag) ) ) { return false; } if ( Array.isArray( Matcher.TagsAny ) && Matcher.TagsAny.length > 0 && !Matcher.TagsAny.some( Tag => Tags.includes(Tag) ) ) { return false; } if ( Array.isArray( Matcher.TagsNot ) && Matcher.TagsNot.some( Tag => Tags.includes(Tag) ) ) { return false; } if ( typeof Matcher.Capture === "string" && Matcher.Capture.length > 0 ) { Captures[ Matcher.Capture ] = CloneMutationPlot( Plot ); } return true; } function IsMutationPlantMature( Plot, Plant, AtTime ) { if ( Number( Plant.GrowthTime ?? 0 ) <= 0 ) { return true; } return ( AtTime - Number( Plot.PlantedAt ?? 0 ) >= Plant.GrowthTime ); } function ApplyMutationFailure( SaveData, Candidate, AtTime = GetSimulationTime( SaveData ) ) { const Failure = Candidate.Mutation.Failure ?? "Keep"; if (Failure === "Keep") { return false; } if (Failure === "Clear") { let Changed = false; for ( const PlotIndex of Candidate.Cells ) { if ( SaveData.Garden.Plots[ PlotIndex ] !== null ) { SaveData.Garden.Plots[ PlotIndex ] = null; Changed = true; } } return Changed; } if (Array.isArray(Failure)) { return ApplyMutationResult( SaveData, Candidate, Failure, AtTime ); } return false; } function ApplyMutationResult( SaveData, Candidate, ResultMatrix, AtTime = GetSimulationTime( SaveData ) ) { if (!Array.isArray(ResultMatrix)) { return false; } const NormalizedResult = NormalizeMutationMatrix( ResultMatrix, Candidate.Pattern[0] ?.length ?? 0, Candidate.Pattern.length, "Keep" ); let Changed = false; const UsedListResultMatches = {}; for ( let LocalY = 0; LocalY < NormalizedResult.length; LocalY++ ) { for ( let LocalX = 0; LocalX < NormalizedResult[ LocalY ].length; LocalX++ ) { const ResultValue = NormalizedResult[ LocalY ][ LocalX ]; if ( ResultValue === null || ResultValue === "Keep" ) { continue; } const PlotIndex = GetGardenPlotIndex( SaveData, Candidate.OriginX + LocalX, Candidate.OriginY + LocalY ); const ListNumber = GetMutationListReference( ResultValue ); let NewPlot; if (ListNumber !== null) { const Match = GetMutationResultListMatch( Candidate, ListNumber, LocalX, LocalY, UsedListResultMatches ); if (Match === undefined) { continue; } NewPlot = CloneMutationPlot( Match.Plot ); } else { NewPlot = CreateMutationResultPlot( ResultValue, Candidate.Captures, AtTime ); } if ( ResultValue === "Empty" || ( ListNumber !== null && NewPlot === null ) ) { if ( SaveData.Garden.Plots[ PlotIndex ] !== null ) { SaveData.Garden.Plots[ PlotIndex ] = null; Changed = true; } continue; } if (NewPlot === undefined) { 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"; } if ( NewPlot !== null && typeof NewPlot === "object" && ExistingPlot !== null && typeof ExistingPlot === "object" && typeof ExistingPlot.AssignedSeed === "string" && typeof CanFunctionalPlantUseAssignedSeed === "function" && CanFunctionalPlantUseAssignedSeed( Plants?.[NewPlot.Plant] ) ) { NewPlot.AssignedSeed = ExistingPlot.AssignedSeed; } SaveData.Garden.Plots[ PlotIndex ] = NewPlot; Changed = true; } } return Changed; } function GetMutationResultListMatch( Candidate, ListNumber, LocalX, LocalY, UsedMatches ) { const Matches = Candidate.ListMatches?.[ ListNumber ] ?? []; UsedMatches[ListNumber] ??= new Set(); const Used = UsedMatches[ListNumber]; let MatchIndex = Matches.findIndex( (Match, Index) => !Used.has(Index) && Match.LocalX === LocalX && Match.LocalY === LocalY ); if (MatchIndex < 0) { MatchIndex = Matches.findIndex( (Match, Index) => !Used.has(Index) ); } if (MatchIndex < 0) { return undefined; } Used.add(MatchIndex); return Matches[MatchIndex]; } function CreateMutationResultPlot( ResultValue, Captures, AtTime ) { if ( typeof ResultValue === "string" ) { if ( ResultValue.startsWith( "$" ) ) { const CaptureName = ResultValue.slice(1); return Captures[ CaptureName ] === undefined ? undefined : CloneMutationPlot( Captures[ CaptureName ] ); } if ( Plants[ResultValue] === undefined ) { return undefined; } return { Plant: ResultValue, PlantedAt: AtTime }; } if ( typeof ResultValue !== "object" || ResultValue === null ) { return undefined; } if ( typeof ResultValue.Plant !== "string" ) { return undefined; } if ( ResultValue.Plant.startsWith( "$" ) ) { const CaptureName = ResultValue.Plant.slice(1); const CapturedPlot = Captures[CaptureName]; if (CapturedPlot === undefined) { return undefined; } return CloneMutationPlot( CapturedPlot ); } if ( Plants[ ResultValue.Plant ] === undefined ) { return undefined; } return { Plant: ResultValue.Plant, PlantedAt: AtTime }; } function DiscoverMutationResult( SaveData, Mutation ) { const NewMutations = []; const NewPlants = []; if ( !HasDiscoveredMutation( SaveData, Mutation.Id ) ) { DiscoverMutation( SaveData, Mutation.Id ); NewMutations.push( Mutation.Id ); } for ( const PlantId of Mutation.Relations ?.PlantsCreated ?? [] ) { if ( HasDiscoveredPlant( SaveData, PlantId ) ) { continue; } DiscoverPlant( SaveData, PlantId ); NewPlants.push( PlantId ); } SaveData.Discoveries.Plants.sort( (A, B) => A - B ); SaveData.Discoveries.Mutations.sort( (A, B) => A - B ); return { Mutations: NewMutations, Plants: NewPlants }; } function CompareMutationCandidates( A, B ) { return ( B.Priority - A.Priority || B.Specificity - A.Specificity || A.Mutation.Id - B.Mutation.Id || A.OriginY - B.OriginY || A.OriginX - B.OriginX || A.Rotation - B.Rotation ); } function GetMutationPatternSpecificity( Pattern, Mutation = null ) { let Specificity = 0; for ( const Row of Pattern ) { for ( const Matcher of Row ) { if ( Matcher === null || Matcher === "Any" ) { continue; } if (Matcher === "Empty") { Specificity += 2; continue; } if ( typeof Matcher === "string" ) { const ListNumber = GetMutationListReference( Matcher ); if (ListNumber !== null) { Specificity += GetMutationListSpecificity( Mutation?.Lists?.[ ListNumber - 1 ] ); continue; } Specificity += 8; continue; } if ( typeof Matcher !== "object" ) { continue; } if ( typeof Matcher.Plant === "string" ) { Specificity += 8; } Specificity += ( Matcher.Tags ?.length ?? 0 ) * 3; Specificity += ( Matcher.TagsAny ?.length ?? 0 ) * 2; Specificity += Matcher.TagsNot ?.length ?? 0; } } return Specificity; } function GetMutationListSpecificity( List ) { if ( List === null || typeof List !== "object" || !Array.isArray(List.Items) || List.Items.length === 0 ) { return 0; } const ItemSpecificities = List.Items.map( Item => { if (Item?.Type === "Empty") { return 2; } if (Item?.Type === "Plant") { return 8; } if (Item?.Type === "Tag") { return 3; } return 0; } ); const Base = Math.min( ...ItemSpecificities ); return Base + (List.Mode === "Once" ? 1 : 0); } function GetMutationCandidateCooldownUntil( SaveData, Candidate ) { return Number( SaveData.MutationCooldowns[ GetMutationCooldownKey( Candidate ) ] ?? 0 ); } function IsMutationCandidateOnCooldown( SaveData, Candidate, AtTime = GetSimulationTime( SaveData ) ) { return ( GetMutationCandidateCooldownUntil( SaveData, Candidate ) > AtTime ); } function StartMutationCandidateCooldown( SaveData, Candidate, AtTime = GetSimulationTime( SaveData ) ) { const Cooldown = Math.max( SimulationMinimumEventInterval, Number( Candidate.Mutation.Cooldown ?? 0 ) || 0 ); SaveData.MutationCooldowns[ GetMutationCooldownKey( Candidate ) ] = AtTime + Cooldown; } function GetMutationCooldownKey( Candidate ) { const Cells = [ ...Candidate.Cells ].sort( (A, B) => A - B ); return ( Candidate.Mutation.Id + ":" + Cells.join(",") ); } function GetMutationOrientations( Mutation ) { const BasePattern = NormalizeMutationMatrix( Mutation.Pattern ?? [], null, null, "Any" ); if (BasePattern.length === 0) { return []; } const BaseSuccess = NormalizeMutationMatrix( Mutation.Success ?? [], BasePattern[0].length, BasePattern.length, "Keep" ); const RotationCount = Mutation.Rotation === "Any" ? 4 : 1; const Orientations = []; const Seen = new Set(); let Pattern = BasePattern; let Success = BaseSuccess; for ( let RotationIndex = 0; RotationIndex < RotationCount; RotationIndex++ ) { const Signature = JSON.stringify(Pattern) + "|" + JSON.stringify(Success); if (!Seen.has(Signature)) { Seen.add(Signature); Orientations.push({ Pattern: Pattern, Success: Success, Rotation: RotationIndex * 90 }); } Pattern = RotateMutationMatrixClockwise( Pattern ); Success = RotateMutationMatrixClockwise( Success ); } return Orientations; } function NormalizeMutationMatrix( Matrix, Width = null, Height = null, FillValue = "Any" ) { if (!Array.isArray(Matrix)) { return []; } const TargetHeight = Height ?? Matrix.length; const TargetWidth = Width ?? Math.max( 0, ...Matrix.map( Row => Array.isArray(Row) ? Row.length : 0 ) ); const Normalized = []; for ( let Y = 0; Y < TargetHeight; Y++ ) { const SourceRow = Array.isArray(Matrix[Y]) ? Matrix[Y] : []; const Row = []; for ( let X = 0; X < TargetWidth; X++ ) { Row.push( X < SourceRow.length ? SourceRow[X] : FillValue ); } Normalized.push( Row ); } return Normalized; } function RotateMutationMatrixClockwise( Matrix ) { if ( Matrix.length === 0 || Matrix[0].length === 0 ) { return []; } const Height = Matrix.length; const Width = Matrix[0].length; const Rotated = []; for ( let X = 0; X < Width; X++ ) { const Row = []; for ( let Y = Height - 1; Y >= 0; Y-- ) { Row.push( Matrix[Y][X] ); } Rotated.push( Row ); } return Rotated; } function GetGardenPlotIndex( SaveData, X, Y ) { return ( Y * SaveData.Garden.Width + X ); } function CloneMutationPlot( Plot ) { if (Plot === null) { return null; } return JSON.parse( JSON.stringify( Plot ) ); }