const FunctionalEffectAreas = [ "Front", "Behind", "Cardinal", "Adjacent", "Farm" ]; const FunctionalEffectActiveTypes = [ "Harvest", "Duplicate", "Plant", "Replant" ]; const FunctionalEffectPassiveTypes = [ "GrowthAdvance", "DisableMutation", "DisableHarvest", "MutationChanceBonus", "CooldownReduction", "HarvestYieldBonus" ]; const FunctionalEffectTypes = [ ...FunctionalEffectActiveTypes, ...FunctionalEffectPassiveTypes ]; const FunctionalEffectMinimumCooldown = 1000; const FunctionalEffectHandlers = new Map(); const FunctionalDirectionOffsets = { North: [0, -1], East: [1, 0], South: [0, 1], West: [-1, 0] }; const FunctionalCardinalOffsets = [ [0, -1], [1, 0], [0, 1], [-1, 0] ]; const FunctionalAdjacentOffsets = [ [0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1] ]; function IsFunctionalPlainObject( Value ) { return ( Value !== null && typeof Value === "object" && !Array.isArray(Value) ); } function GetPlantFunctionalEffects( Plant ) { const Functional = Plant?.Effects?.Functional; if (!Array.isArray(Functional)) { return []; } return Functional.filter( Effect => IsFunctionalPlainObject( Effect ) && typeof Effect.Type === "string" && Effect.Type.trim().length > 0 ); } function IsFunctionalPlant( Plant ) { return ( GetPlantFunctionalEffects( Plant ).length > 0 ); } function IsKnownFunctionalEffectType( Type ) { return FunctionalEffectTypes.includes( String(Type ?? "") ); } function IsFunctionalEffectActive( Effect ) { return FunctionalEffectActiveTypes.includes( String(Effect?.Type ?? "") ); } function IsFunctionalEffectPassive( Effect ) { return FunctionalEffectPassiveTypes.includes( String(Effect?.Type ?? "") ); } function GetFunctionalEffectArea( Effect, Field = "Area", Fallback = "Front" ) { const Area = String( Effect?.[Field] ?? Fallback ); return FunctionalEffectAreas.includes( Area ) ? Area : Fallback; } function DoesFunctionalEffectRequireMaturity( Effect ) { return Effect?.RequireMature !== false; } function GetFunctionalEffectCooldown( Effect ) { const Cooldown = Number( Effect?.Cooldown ?? 0 ); if ( !Number.isFinite(Cooldown) || Cooldown <= 0 ) { return 0; } return Math.max( FunctionalEffectMinimumCooldown, Math.floor(Cooldown) ); } function GetResolvedFunctionalEffectCooldown( SaveData, GardenIndex, PlotIndex, Effect, AtTime ) { return GetFunctionalEffectCooldown( Effect ); } function GetFunctionalEffectAmount( Effect, Fallback = 0 ) { const Amount = Number( Effect?.Amount ); return Number.isFinite(Amount) ? Amount : Fallback; } function GetFunctionalEffectKey( Effect, EffectIndex ) { const ExplicitKey = typeof Effect?.Key === "string" ? Effect.Key.trim() : ""; if (ExplicitKey.length > 0) { return ExplicitKey; } return ( String( Effect?.Type ?? "Effect" ) + ":" + String(EffectIndex) ); } function RegisterFunctionalEffectHandler( Type, Handler ) { const NormalizedType = String(Type ?? "").trim(); if ( NormalizedType.length === 0 || typeof Handler !== "function" ) { return false; } FunctionalEffectHandlers.set( NormalizedType, Handler ); return true; } function UnregisterFunctionalEffectHandler( Type ) { return FunctionalEffectHandlers.delete( String(Type ?? "") ); } function GetFunctionalEffectHandler( Type ) { return FunctionalEffectHandlers.get( String(Type ?? "") ) ?? null; } function HasFunctionalEffectHandler( Type ) { return ( GetFunctionalEffectHandler( Type ) !== null ); } function GetFunctionalGarden( SaveData, GardenIndex ) { if ( !Array.isArray( SaveData?.Gardens ) ) { return null; } return SaveData.Gardens[ GardenIndex ] ?? null; } function GetFunctionalPlotCoordinates( Garden, PlotIndex ) { const Width = Math.max( 1, Math.floor( Number(Garden?.Width) || 1 ) ); return { X: PlotIndex % Width, Y: Math.floor( PlotIndex / Width ) }; } function GetFunctionalPlotIndex( Garden, X, Y ) { const Width = Math.max( 1, Math.floor( Number(Garden?.Width) || 1 ) ); const Height = Math.max( 1, Math.floor( Number(Garden?.Height) || 1 ) ); if ( X < 0 || Y < 0 || X >= Width || Y >= Height ) { return null; } return Y * Width + X; } function GetFunctionalPlotRotation( Plot ) { const Rotation = String( Plot?.Rotation ?? "East" ); return FunctionalDirectionOffsets[ Rotation ] !== undefined ? Rotation : "East"; } function GetFunctionalOffsetIndexes( Garden, PlotIndex, Offsets ) { const Coordinates = GetFunctionalPlotCoordinates( Garden, PlotIndex ); const Results = []; for (const [OffsetX, OffsetY] of Offsets) { const TargetIndex = GetFunctionalPlotIndex( Garden, Coordinates.X + OffsetX, Coordinates.Y + OffsetY ); if (TargetIndex !== null) { Results.push(TargetIndex); } } return Results; } function GetFunctionalAreaPlotIndexes( SaveData, GardenIndex, OriginPlotIndex, Area, Rotation = null ) { const Garden = GetFunctionalGarden( SaveData, GardenIndex ); if (Garden === null) { return []; } const NormalizedArea = FunctionalEffectAreas.includes( Area ) ? Area : "Front"; if (NormalizedArea === "Farm") { return Array.from( { length: Garden.Plots?.length ?? 0 }, (_, PlotIndex) => PlotIndex ); } if (NormalizedArea === "Cardinal") { return GetFunctionalOffsetIndexes( Garden, OriginPlotIndex, FunctionalCardinalOffsets ); } if (NormalizedArea === "Adjacent") { return GetFunctionalOffsetIndexes( Garden, OriginPlotIndex, FunctionalAdjacentOffsets ); } const Direction = Rotation === null ? GetFunctionalPlotRotation( Garden.Plots?.[ OriginPlotIndex ] ) : GetFunctionalPlotRotation({ Rotation }); const [DirectionX, DirectionY] = FunctionalDirectionOffsets[ Direction ]; const Multiplier = NormalizedArea === "Behind" ? -1 : 1; return GetFunctionalOffsetIndexes( Garden, OriginPlotIndex, [[ DirectionX * Multiplier, DirectionY * Multiplier ]] ); } function GetFunctionalAreaTargets( SaveData, GardenIndex, OriginPlotIndex, Area, Rotation = null ) { const Garden = GetFunctionalGarden( SaveData, GardenIndex ); if (Garden === null) { return []; } return GetFunctionalAreaPlotIndexes( SaveData, GardenIndex, OriginPlotIndex, Area, Rotation ).map( PlotIndex => ({ GardenIndex, PlotIndex, Plot: Garden.Plots[ PlotIndex ] ?? null }) ); } function IsFunctionalPlantMatureAtTime( Plot, Plant, AtTime ) { if ( Plot === null || Plant === null || Plant === undefined ) { return false; } const GrowthTime = Math.max( 0, Number( Plant.GrowthTime ?? 0 ) || 0 ); return ( Number(AtTime) - Number( Plot.PlantedAt ?? 0 ) >= GrowthTime ); } function EnsureFunctionalCooldowns( Plot ) { if ( Plot === null || typeof Plot !== "object" || Array.isArray(Plot) ) { return null; } if ( !IsFunctionalPlainObject( Plot.FunctionalCooldowns ) ) { Plot.FunctionalCooldowns = {}; } return Plot.FunctionalCooldowns; } function NormalizeFunctionalCooldownEntry( Value ) { if ( Number.isFinite( Number(Value) ) ) { return { StartedAt: 0, Until: Math.max( 0, Math.floor( Number(Value) ) ) }; } if (!IsFunctionalPlainObject(Value)) { return null; } const StartedAt = Math.max( 0, Math.floor( Number( Value.StartedAt ?? 0 ) || 0 ) ); const Until = Math.max( StartedAt, Math.floor( Number( Value.Until ?? 0 ) || 0 ) ); if (Until <= 0) { return null; } return { StartedAt, Until }; } function GetFunctionalEffectCooldownState( Plot, Effect, EffectIndex, AtTime ) { const Cooldowns = EnsureFunctionalCooldowns( Plot ); if (Cooldowns === null) { return null; } const EffectKey = GetFunctionalEffectKey( Effect, EffectIndex ); const Entry = NormalizeFunctionalCooldownEntry( Cooldowns[EffectKey] ); if (Entry === null) { delete Cooldowns[EffectKey]; return null; } Cooldowns[EffectKey] = Entry; const Duration = Math.max( 1, Entry.Until - Entry.StartedAt ); const Progress = Math.max( 0, Math.min( ( Number(AtTime) - Entry.StartedAt ) / Duration, 1 ) ); return { Key: EffectKey, StartedAt: Entry.StartedAt, Until: Entry.Until, Remaining: Math.max( 0, Entry.Until - Number(AtTime) ), Progress, Ready: Number(AtTime) >= Entry.Until }; } function StartFunctionalEffectCooldown( Plot, Effect, EffectIndex, AtTime, ResolvedCooldown = null ) { const Cooldown = ResolvedCooldown === null ? GetFunctionalEffectCooldown( Effect ) : Math.max( 0, Math.floor( Number( ResolvedCooldown ) || 0 ) ); if (Cooldown <= 0) { return null; } const Cooldowns = EnsureFunctionalCooldowns( Plot ); if (Cooldowns === null) { return null; } const EffectKey = GetFunctionalEffectKey( Effect, EffectIndex ); const StartedAt = Math.max( 0, Math.floor( Number(AtTime) || 0 ) ); Cooldowns[EffectKey] = { StartedAt, Until: StartedAt + Cooldown }; return GetFunctionalEffectCooldownState( Plot, Effect, EffectIndex, AtTime ); } function IsFunctionalEffectOnCooldown( Plot, Effect, EffectIndex, AtTime ) { const State = GetFunctionalEffectCooldownState( Plot, Effect, EffectIndex, AtTime ); return ( State !== null && !State.Ready ); } function GetFunctionalPlantCooldownDisplayState( Plot, Plant, AtTime ) { if ( !IsFunctionalPlantMatureAtTime( Plot, Plant, AtTime ) ) { return null; } let BestState = null; const Effects = GetPlantFunctionalEffects( Plant ); Effects.forEach( (Effect, EffectIndex) => { if ( !IsFunctionalEffectActive( Effect ) ) { return; } const State = GetFunctionalEffectCooldownState( Plot, Effect, EffectIndex, AtTime ); if ( State === null || State.Ready ) { return; } if ( BestState === null || State.Until < BestState.Until ) { BestState = { ...State, Effect, EffectIndex }; } } ); return BestState; } function GetFunctionalEffectsAffectingPlot( SaveData, GardenIndex, TargetPlotIndex, Type, AtTime ) { const Garden = GetFunctionalGarden( SaveData, GardenIndex ); if (Garden === null) { return []; } const Results = []; Garden.Plots.forEach( (SourcePlot, SourcePlotIndex) => { if (SourcePlot === null) { return; } const SourcePlant = Plants?.[ SourcePlot.Plant ]; if (SourcePlant === undefined) { return; } const Effects = GetPlantFunctionalEffects( SourcePlant ); Effects.forEach( (Effect, EffectIndex) => { if ( String( Effect.Type ) !== String(Type) ) { return; } if ( DoesFunctionalEffectRequireMaturity( Effect ) && !IsFunctionalPlantMatureAtTime( SourcePlot, SourcePlant, AtTime ) ) { return; } const Area = GetFunctionalEffectArea( Effect ); const Targets = GetFunctionalAreaPlotIndexes( SaveData, GardenIndex, SourcePlotIndex, Area ); if ( !Targets.includes( TargetPlotIndex ) ) { return; } Results.push({ GardenIndex, SourcePlotIndex, SourcePlot, SourcePlant, Effect, EffectIndex }); } ); } ); return Results; } function GetStrongestFunctionalEffectAffectingPlot( SaveData, GardenIndex, TargetPlotIndex, Type, AtTime ) { const Effects = GetFunctionalEffectsAffectingPlot( SaveData, GardenIndex, TargetPlotIndex, Type, AtTime ); if (Effects.length === 0) { return null; } return Effects.reduce( (Best, Candidate) => Best === null || GetFunctionalEffectAmount( Candidate.Effect ) > GetFunctionalEffectAmount( Best.Effect ) ? Candidate : Best, null ); } function GetStrongestFunctionalEffectAffectingAnyPlot( SaveData, GardenIndex, TargetPlotIndexes, Type, AtTime ) { let Best = null; for (const PlotIndex of TargetPlotIndexes) { const Candidate = GetStrongestFunctionalEffectAffectingPlot( SaveData, GardenIndex, PlotIndex, Type, AtTime ); if ( Candidate !== null && ( Best === null || GetFunctionalEffectAmount( Candidate.Effect ) > GetFunctionalEffectAmount( Best.Effect ) ) ) { Best = Candidate; } } return Best; } function CreateFunctionalProcessingResult() { return { Attempted: false, Changed: false, Activations: [] }; } function MergeFunctionalHandlerResult( Result, HandlerResult ) { if (HandlerResult === true) { Result.Changed = true; return; } if (!IsFunctionalPlainObject(HandlerResult)) { return; } Result.Changed = Result.Changed || HandlerResult.Changed === true; } function ProcessFunctionalEffectsAtTime( SaveData, AtTime ) { const Result = CreateFunctionalProcessingResult(); if ( !Array.isArray( SaveData?.Gardens ) ) { return Result; } for ( let GardenIndex = 0; GardenIndex < SaveData.Gardens.length; GardenIndex++ ) { const Garden = SaveData.Gardens[ GardenIndex ]; for ( let PlotIndex = 0; PlotIndex < Garden.Plots.length; PlotIndex++ ) { const Plot = Garden.Plots[ PlotIndex ]; if (Plot === null) { continue; } const Plant = Plants?.[Plot.Plant]; if (Plant === undefined) { continue; } const Effects = GetPlantFunctionalEffects( Plant ); for ( let EffectIndex = 0; EffectIndex < Effects.length; EffectIndex++ ) { const Effect = Effects[EffectIndex]; if ( !IsFunctionalEffectActive( Effect ) ) { continue; } if ( DoesFunctionalEffectRequireMaturity( Effect ) && !IsFunctionalPlantMatureAtTime( Plot, Plant, AtTime ) ) { continue; } const Handler = GetFunctionalEffectHandler( Effect.Type ); if (Handler === null) { continue; } if ( IsFunctionalEffectOnCooldown( Plot, Effect, EffectIndex, AtTime ) ) { continue; } Result.Attempted = true; const ResolvedCooldown = GetResolvedFunctionalEffectCooldown( SaveData, GardenIndex, PlotIndex, Effect, AtTime ); StartFunctionalEffectCooldown( Plot, Effect, EffectIndex, AtTime, ResolvedCooldown ); const HandlerResult = Handler({ SaveData, GardenIndex, Garden, PlotIndex, Plot, Plant, Effect, EffectIndex, AtTime, GetTargets: ( Area = null ) => GetFunctionalAreaTargets( SaveData, GardenIndex, PlotIndex, Area ?? GetFunctionalEffectArea( Effect ) ) }); MergeFunctionalHandlerResult( Result, HandlerResult ); Result.Activations.push({ GardenIndex, PlotIndex, PlantKey: Plot.Plant, Type: Effect.Type, EffectIndex, AtTime }); if ( Garden.Plots[PlotIndex] !== Plot ) { break; } } } } return Result; } function GetNextFunctionalEffectEventTime( SaveData, CurrentTime, TargetTime ) { if ( !Array.isArray( SaveData?.Gardens ) ) { return null; } let NextTime = null; for ( let GardenIndex = 0; GardenIndex < SaveData.Gardens.length; GardenIndex++ ) { const Garden = SaveData.Gardens[ GardenIndex ]; for ( let PlotIndex = 0; PlotIndex < Garden.Plots.length; PlotIndex++ ) { const Plot = Garden.Plots[ PlotIndex ]; if (Plot === null) { continue; } const Plant = Plants?.[Plot.Plant]; if (Plant === undefined) { continue; } const Effects = GetPlantFunctionalEffects( Plant ); if (Effects.length === 0) { continue; } const MaturityTime = Number( Plot.PlantedAt ?? 0 ) + Math.max( 0, Number( Plant.GrowthTime ?? 0 ) || 0 ); for ( let EffectIndex = 0; EffectIndex < Effects.length; EffectIndex++ ) { const Effect = Effects[EffectIndex]; if ( !IsFunctionalEffectActive( Effect ) || !HasFunctionalEffectHandler( Effect.Type ) ) { continue; } let EventTime; if ( DoesFunctionalEffectRequireMaturity( Effect ) && MaturityTime > CurrentTime ) { EventTime = MaturityTime; } else { const CooldownState = GetFunctionalEffectCooldownState( Plot, Effect, EffectIndex, CurrentTime ); if ( CooldownState === null || CooldownState.Ready ) { EventTime = CurrentTime; } else { EventTime = CooldownState.Until; } } if ( EventTime <= CurrentTime || EventTime > TargetTime ) { continue; } if ( NextTime === null || EventTime < NextTime ) { NextTime = EventTime; } } } } return NextTime; }