diff --git a/Scripts/Garden/AdminValidation.js b/Scripts/Garden/AdminValidation.js index 6511570..1cf775d 100644 --- a/Scripts/Garden/AdminValidation.js +++ b/Scripts/Garden/AdminValidation.js @@ -98,13 +98,29 @@ function ValidateAdminFunctionalEffects( ); } + const EffectType = String( + Effect.Type ?? "" + ).trim(); + ValidateAdminKey( - String( - Effect.Type ?? "" - ).trim(), + EffectType, "Functional effect type" ); + if ( + typeof IsKnownFunctionalEffectType === + "function" && + !IsKnownFunctionalEffectType( + EffectType + ) + ) { + throw new Error( + "Unknown functional effect type: " + + EffectType + + "." + ); + } + if (Effect.Key !== undefined) { const EffectKey = String( Effect.Key ?? "" @@ -175,16 +191,41 @@ function ValidateAdminFunctionalEffects( "Functional effect amount must be a number." ); } + + if ( + [ + "GrowthAdvance", + "MutationChanceBonus", + "CooldownReduction", + "HarvestYieldBonus" + ].includes(EffectType) && + (Amount < 0 || Amount > 1) + ) { + throw new Error( + "Functional percentage amounts must be between 0 and 1." + ); + } } - if ( - Effect.RequireMature !== undefined && - typeof Effect.RequireMature !== - "boolean" - ) { - throw new Error( - "Functional effect RequireMature must be On or Off." - ); + for (const BooleanField of [ + "RequireMature", + "RequireMatureSource", + "RandomSource", + "RandomTarget", + "ExcludeFunctional", + "ConsumeSeed" + ]) { + if ( + Effect[BooleanField] !== undefined && + typeof Effect[BooleanField] !== + "boolean" + ) { + throw new Error( + "Functional effect " + + BooleanField + + " must be On or Off." + ); + } } if ( diff --git a/Scripts/Garden/FunctionalEffects.js b/Scripts/Garden/FunctionalEffects.js index 44b0c63..469a8ad 100644 --- a/Scripts/Garden/FunctionalEffects.js +++ b/Scripts/Garden/FunctionalEffects.js @@ -180,8 +180,40 @@ function GetResolvedFunctionalEffectCooldown( Effect, AtTime ) { - return GetFunctionalEffectCooldown( - Effect + const BaseCooldown = + GetFunctionalEffectCooldown( + Effect + ); + + if (BaseCooldown <= 0) { + return 0; + } + + const Accelerator = + GetStrongestFunctionalEffectAffectingPlot( + SaveData, + GardenIndex, + PlotIndex, + "CooldownReduction", + AtTime + ); + + const Reduction = Math.max( + 0, + Math.min( + 1, + GetFunctionalEffectAmount( + Accelerator?.Effect + ) + ) + ); + + return Math.max( + FunctionalEffectMinimumCooldown, + Math.floor( + BaseCooldown * + (1 - Reduction) + ) ); } @@ -1002,6 +1034,833 @@ function GetStrongestFunctionalEffectAffectingAnyPlot( } + +function DoesFunctionalPlantHaveEffectType( + Plant, + Type +) { + return GetPlantFunctionalEffects( + Plant + ).some( + Effect => + String(Effect.Type) === + String(Type) + ); +} + + +function CanFunctionalPlantUseAssignedSeed( + Plant +) { + return DoesFunctionalPlantHaveEffectType( + Plant, + "Plant" + ); +} + + +function GetFunctionalPercentageAmount( + Effect, + Fallback = 0 +) { + return Math.max( + 0, + Math.min( + 1, + GetFunctionalEffectAmount( + Effect, + Fallback + ) + ) + ); +} + + +function GetFunctionalRandomItem( + SaveData, + Items +) { + if (!Array.isArray(Items) || Items.length === 0) { + return null; + } + + if (Items.length === 1) { + return Items[0]; + } + + const Random = + typeof GetSimulationRandom === "function" + ? GetSimulationRandom( + SaveData + ) + : Math.random(); + + return Items[ + Math.min( + Items.length - 1, + Math.floor( + Random * Items.length + ) + ) + ]; +} + + +function AddFunctionalCurrencyReward( + SaveData, + Currency, + Amount +) { + const RewardAmount = Number( + Amount + ); + + if ( + !Number.isFinite(RewardAmount) || + RewardAmount <= 0 + ) { + return 0; + } + + SaveData.Currency ??= {}; + SaveData.Currency[Currency] ??= 0; + SaveData.Currency[Currency] += RewardAmount; + + SaveData.Statistics ??= {}; + SaveData.Statistics.CurrencyEarned ??= {}; + SaveData.Statistics.CurrencyEarned[ + Currency + ] ??= 0; + SaveData.Statistics.CurrencyEarned[ + Currency + ] += RewardAmount; + + return RewardAmount; +} + + +function IsGardenPlotHarvestDisabled( + SaveData, + GardenIndex, + PlotIndex, + AtTime +) { + return ( + GetStrongestFunctionalEffectAffectingPlot( + SaveData, + GardenIndex, + PlotIndex, + "DisableHarvest", + AtTime + ) !== null + ); +} + + +function IsGardenPlotMutationDisabled( + SaveData, + GardenIndex, + PlotIndex, + AtTime +) { + return ( + GetStrongestFunctionalEffectAffectingPlot( + SaveData, + GardenIndex, + PlotIndex, + "DisableMutation", + AtTime + ) !== null + ); +} + + +function DoesMutationPatternInvolveDisableMutation( + SaveData, + GardenIndex, + OriginX, + OriginY, + Pattern +) { + const Garden = + GetFunctionalGarden( + SaveData, + GardenIndex + ); + + if ( + Garden === null || + !Array.isArray(Pattern) + ) { + return false; + } + + for ( + let LocalY = 0; + LocalY < Pattern.length; + LocalY++ + ) { + const Row = Pattern[LocalY]; + + if (!Array.isArray(Row)) { + continue; + } + + for ( + let LocalX = 0; + LocalX < Row.length; + LocalX++ + ) { + const Matcher = Row[LocalX]; + + if ( + Matcher === null || + Matcher === "Any" || + Matcher === "Empty" + ) { + continue; + } + + const PlotIndex = + GetFunctionalPlotIndex( + Garden, + OriginX + LocalX, + OriginY + LocalY + ); + + if (PlotIndex === null) { + continue; + } + + const Plot = + Garden.Plots?.[PlotIndex] ?? null; + + if (Plot === null) { + continue; + } + + const Plant = + Plants?.[Plot.Plant]; + + if ( + Plant !== undefined && + DoesFunctionalPlantHaveEffectType( + Plant, + "DisableMutation" + ) + ) { + return true; + } + } + } + + return false; +} + + +function GetGardenPlotGrowthAdvance( + SaveData, + GardenIndex, + PlotIndex, + AtTime +) { + const Effect = + GetStrongestFunctionalEffectAffectingPlot( + SaveData, + GardenIndex, + PlotIndex, + "GrowthAdvance", + AtTime + ); + + return GetFunctionalPercentageAmount( + Effect?.Effect + ); +} + + +function GetGardenPlotHarvestYieldBonus( + SaveData, + GardenIndex, + PlotIndex, + AtTime +) { + const Effect = + GetStrongestFunctionalEffectAffectingPlot( + SaveData, + GardenIndex, + PlotIndex, + "HarvestYieldBonus", + AtTime + ); + + return GetFunctionalPercentageAmount( + Effect?.Effect + ); +} + + +function PlantGardenPlot( + SaveData, + GardenIndex, + PlotIndex, + PlantKey, + AtTime, + Options = {} +) { + const Garden = + GetFunctionalGarden( + SaveData, + GardenIndex + ); + + const Plant = + Plants?.[PlantKey]; + + if ( + Garden === null || + Plant === undefined || + Garden.Plots?.[PlotIndex] !== null + ) { + return { + Changed: false, + Planted: false + }; + } + + const ConsumeSeed = + Options.ConsumeSeed === true; + + if (ConsumeSeed) { + if ( + typeof TakeSeed !== "function" || + !TakeSeed( + SaveData, + Plant.Id, + 1 + ) + ) { + return { + Changed: false, + Planted: false, + MissingSeed: true + }; + } + } + + const GrowthAdvance = + Options.ApplyGrowthAdvance === false + ? 0 + : GetGardenPlotGrowthAdvance( + SaveData, + GardenIndex, + PlotIndex, + AtTime + ); + + const GrowthTime = Math.max( + 0, + Number( + Plant.GrowthTime ?? 0 + ) || 0 + ); + + const PlantedAt = + Number(AtTime) - + GrowthTime * GrowthAdvance; + + const Rotation = + FunctionalDirectionOffsets[ + Options.Rotation + ] !== undefined + ? Options.Rotation + : "East"; + + Garden.Plots[PlotIndex] = { + Plant: PlantKey, + PlantedAt, + Rotation + }; + + return { + Changed: true, + Planted: true, + PlantKey, + PlotIndex, + GrowthAdvance, + PlantedAt + }; +} + + +function HarvestGardenPlot( + SaveData, + GardenIndex, + PlotIndex, + AtTime, + Options = {} +) { + const Garden = + GetFunctionalGarden( + SaveData, + GardenIndex + ); + + const Plot = + Garden?.Plots?.[PlotIndex] ?? null; + + if (Plot === null) { + return { + Changed: false, + Harvested: false + }; + } + + const Plant = + Plants?.[Plot.Plant]; + + if ( + Plant === undefined || + !IsFunctionalPlantMatureAtTime( + Plot, + Plant, + AtTime + ) + ) { + return { + Changed: false, + Harvested: false, + Immature: Plant !== undefined + }; + } + + if ( + Options.ExcludeFunctional === true && + IsFunctionalPlant( + Plant + ) + ) { + return { + Changed: false, + Harvested: false, + Excluded: true + }; + } + + if ( + IsGardenPlotHarvestDisabled( + SaveData, + GardenIndex, + PlotIndex, + AtTime + ) + ) { + return { + Changed: false, + Harvested: false, + Disabled: true + }; + } + + const BaseReward = + typeof GetPlantHarvestReward === "function" + ? GetPlantHarvestReward( + SaveData, + Plant.Id + ) + : null; + + if (BaseReward === null) { + return { + Changed: false, + Harvested: false, + RewardUnavailable: true + }; + } + + const YieldBonus = + GetGardenPlotHarvestYieldBonus( + SaveData, + GardenIndex, + PlotIndex, + AtTime + ); + + const RewardAmount = Math.ceil( + BaseReward * + (1 + YieldBonus) + ); + + AddFunctionalCurrencyReward( + SaveData, + "Dew", + RewardAmount + ); + + Garden.Plots[PlotIndex] = null; + + return { + Changed: true, + Harvested: true, + Plant, + PlantKey: Plot.Plant, + Rotation: + GetFunctionalPlotRotation( + Plot + ), + RewardAmount, + YieldBonus + }; +} + + +function GetFunctionalEligibleSourceTargets( + SaveData, + Targets, + AtTime, + RequireMature = true +) { + return Targets.filter( + Target => { + const Plot = Target.Plot; + + if (Plot === null) { + return false; + } + + const Plant = + Plants?.[Plot.Plant]; + + if (Plant === undefined) { + return false; + } + + return ( + !RequireMature || + IsFunctionalPlantMatureAtTime( + Plot, + Plant, + AtTime + ) + ); + } + ); +} + + +function SelectFunctionalTarget( + SaveData, + Targets, + Random +) { + if (Targets.length === 0) { + return null; + } + + return Random === true + ? GetFunctionalRandomItem( + SaveData, + Targets + ) + : Targets[0]; +} + + +function HandleFunctionalHarvest( + Context +) { + const Targets = + Context.GetTargets(); + + let Changed = false; + + for (const Target of Targets) { + const Result = + HarvestGardenPlot( + Context.SaveData, + Target.GardenIndex, + Target.PlotIndex, + Context.AtTime, + { + ExcludeFunctional: + Context.Effect.ExcludeFunctional === true + } + ); + + Changed = + Changed || + Result.Changed === true; + } + + return {Changed}; +} + + +function HandleFunctionalDuplicate( + Context +) { + const SourceArea = + GetFunctionalEffectArea( + Context.Effect, + "Source", + "Behind" + ); + + const TargetArea = + GetFunctionalEffectArea( + Context.Effect, + "Target", + "Front" + ); + + const Sources = + GetFunctionalEligibleSourceTargets( + Context.SaveData, + GetFunctionalAreaTargets( + Context.SaveData, + Context.GardenIndex, + Context.PlotIndex, + SourceArea + ), + Context.AtTime, + Context.Effect.RequireMatureSource !== false + ); + + const EmptyTargets = + GetFunctionalAreaTargets( + Context.SaveData, + Context.GardenIndex, + Context.PlotIndex, + TargetArea + ).filter( + Target => Target.Plot === null + ); + + const Source = + SelectFunctionalTarget( + Context.SaveData, + Sources, + Context.Effect.RandomSource === true + ); + + const Target = + SelectFunctionalTarget( + Context.SaveData, + EmptyTargets, + Context.Effect.RandomTarget === true + ); + + if ( + Source === null || + Target === null + ) { + return {Changed: false}; + } + + return PlantGardenPlot( + Context.SaveData, + Context.GardenIndex, + Target.PlotIndex, + Source.Plot.Plant, + Context.AtTime, + { + ConsumeSeed: false + } + ); +} + + +function HandleFunctionalPlant( + Context +) { + const AssignedSeed = + typeof Context.Plot.AssignedSeed === "string" + ? Context.Plot.AssignedSeed + : null; + + if ( + AssignedSeed === null || + Plants?.[AssignedSeed] === undefined + ) { + return {Changed: false}; + } + + const TargetArea = + GetFunctionalEffectArea( + Context.Effect, + "Target", + "Front" + ); + + const EmptyTargets = + GetFunctionalAreaTargets( + Context.SaveData, + Context.GardenIndex, + Context.PlotIndex, + TargetArea + ).filter( + Target => Target.Plot === null + ); + + const Target = + SelectFunctionalTarget( + Context.SaveData, + EmptyTargets, + Context.Effect.RandomTarget === true + ); + + if (Target === null) { + return {Changed: false}; + } + + return PlantGardenPlot( + Context.SaveData, + Context.GardenIndex, + Target.PlotIndex, + AssignedSeed, + Context.AtTime, + { + ConsumeSeed: true + } + ); +} + + +function HandleFunctionalReplant( + Context +) { + const Targets = + Context.GetTargets(); + + let Changed = false; + + for (const Target of Targets) { + const ExistingPlot = Target.Plot; + + if (ExistingPlot === null) { + continue; + } + + const ExistingPlant = + Plants?.[ExistingPlot.Plant]; + + if (ExistingPlant === undefined) { + continue; + } + + const ConsumeSeed = + Context.Effect.ConsumeSeed !== false; + + if ( + ConsumeSeed && + ( + typeof GetSeedCount !== "function" || + GetSeedCount( + Context.SaveData, + ExistingPlant.Id + ) < 1 + ) + ) { + continue; + } + + const Rotation = + GetFunctionalPlotRotation( + ExistingPlot + ); + + const HarvestResult = + HarvestGardenPlot( + Context.SaveData, + Target.GardenIndex, + Target.PlotIndex, + Context.AtTime, + { + ExcludeFunctional: + Context.Effect.ExcludeFunctional === true + } + ); + + if (!HarvestResult.Harvested) { + continue; + } + + const PlantResult = + PlantGardenPlot( + Context.SaveData, + Target.GardenIndex, + Target.PlotIndex, + HarvestResult.PlantKey, + Context.AtTime, + { + ConsumeSeed, + Rotation + } + ); + + Changed = + Changed || + HarvestResult.Changed === true || + PlantResult.Changed === true; + } + + return {Changed}; +} + + +function SetFunctionalPlantAssignedSeed( + Plot, + PlantKey +) { + if ( + Plot === null || + typeof Plot !== "object" || + Plants?.[PlantKey] === undefined + ) { + return false; + } + + Plot.AssignedSeed = PlantKey; + return true; +} + + +function ClearFunctionalPlantAssignedSeed( + Plot +) { + if ( + Plot === null || + typeof Plot !== "object" + ) { + return false; + } + + delete Plot.AssignedSeed; + return true; +} + + +RegisterFunctionalEffectHandler( + "Harvest", + HandleFunctionalHarvest +); + +RegisterFunctionalEffectHandler( + "Duplicate", + HandleFunctionalDuplicate +); + +RegisterFunctionalEffectHandler( + "Plant", + HandleFunctionalPlant +); + +RegisterFunctionalEffectHandler( + "Replant", + HandleFunctionalReplant +); + + function CreateFunctionalProcessingResult() { return { Attempted: false, diff --git a/Scripts/Garden/Garden.js b/Scripts/Garden/Garden.js index b0ec523..a4b0e9a 100644 --- a/Scripts/Garden/Garden.js +++ b/Scripts/Garden/Garden.js @@ -2185,28 +2185,27 @@ async function PlantSeed( } - if ( - !TakeSeed( + const PlantResult = + PlantGardenPlot( GameSave, - SelectedSeedId - ) - ) { + GameSave.ActiveGardenIndex, + PlotIndex, + PlantKey, + ActionTime, + { + ConsumeSeed: true, + Rotation: + GardenPlotDefaultRotation + } + ); + + if (!PlantResult.Planted) { EnsureSelectedSeed(); RenderSeeds(); - return; } - GameSave.Garden.Plots[ - PlotIndex - ] = { - Plant: PlantKey, - PlantedAt: ActionTime, - Rotation: GardenPlotDefaultRotation - }; - - SetGardenMessage( "Planted " + Plant.Name + @@ -2307,40 +2306,47 @@ async function PlantManySeeds() { } - if ( - !TakeSeed( - GameSave, - SelectedSeedId, - PlantCount - ) - ) { - return; - } - - - const PlantedAt = ActionTime; + let PlantedCount = 0; for ( let Index = 0; Index < PlantCount; Index++ ) { - GameSave.Garden.Plots[ - EmptyPlotIndexes[Index] - ] = { - Plant: PlantKey, - PlantedAt: PlantedAt, - Rotation: GardenPlotDefaultRotation - }; + const PlantResult = + PlantGardenPlot( + GameSave, + GameSave.ActiveGardenIndex, + EmptyPlotIndexes[Index], + PlantKey, + ActionTime, + { + ConsumeSeed: true, + Rotation: + GardenPlotDefaultRotation + } + ); + + if (!PlantResult.Planted) { + break; + } + + PlantedCount++; + } + + if (PlantedCount <= 0) { + EnsureSelectedSeed(); + RenderSeeds(); + return; } SetGardenMessage( "Magic trowel planted " + - PlantCount.toLocaleString() + + PlantedCount.toLocaleString() + " " + Plant.Name + - (PlantCount === 1 + (PlantedCount === 1 ? " seed." : " seeds.") ); @@ -2391,38 +2397,40 @@ async function HarvestPlant( } - const RewardAmount = - GetPlantHarvestReward( + const HarvestResult = + HarvestGardenPlot( GameSave, - Plant.Id + GameSave.ActiveGardenIndex, + PlotIndex, + ActionTime ); - if (RewardAmount === null) { - SetGardenMessage( - "Couldn't calculate the harvest reward for " + - Plant.Name + - "." - ); + if (!HarvestResult.Harvested) { + if (HarvestResult.Disabled) { + SetGardenMessage( + Plant.Name + + " can't be harvested right now." + ); + } else if ( + HarvestResult.RewardUnavailable + ) { + SetGardenMessage( + "Couldn't calculate the harvest reward for " + + Plant.Name + + "." + ); + } + RenderGame(); return; } - GiveReward({ - Currency: "Dew", - Amount: RewardAmount - }); - - GameSave.Garden.Plots[ - PlotIndex - ] = null; - - SetGardenMessage( "Harvested " + Plant.Name + " for " + - RewardAmount + + HarvestResult.RewardAmount + " Dew." ); diff --git a/Scripts/Garden/MutationEngine.js b/Scripts/Garden/MutationEngine.js index b29a76d..31b7823 100644 --- a/Scripts/Garden/MutationEngine.js +++ b/Scripts/Garden/MutationEngine.js @@ -131,7 +131,7 @@ function CheckGardenMutations( ); - const Chance = Math.max( + const BaseChance = Math.max( 0, Math.min( Number( @@ -142,6 +142,31 @@ function CheckGardenMutations( ) ); + 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( @@ -386,6 +411,42 @@ function CreateMutationCandidate( } + 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, @@ -789,6 +850,23 @@ function ApplyMutationResult( : "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 diff --git a/Scripts/Garden/Save.js b/Scripts/Garden/Save.js index 6760195..c7cc003 100644 --- a/Scripts/Garden/Save.js +++ b/Scripts/Garden/Save.js @@ -288,6 +288,17 @@ function NormalizeGardenData( ? Plot.Rotation : "East"; + if ( + typeof Plot.AssignedSeed === + "string" && + Plot.AssignedSeed.trim().length > 0 + ) { + Plot.AssignedSeed = + Plot.AssignedSeed.trim(); + } else { + delete Plot.AssignedSeed; + } + if ( Plot.FunctionalCooldowns === null || typeof Plot.FunctionalCooldowns !== diff --git a/Scripts/Garden/Simulation.js b/Scripts/Garden/Simulation.js index 59909a2..8898922 100644 --- a/Scripts/Garden/Simulation.js +++ b/Scripts/Garden/Simulation.js @@ -1,5 +1,6 @@ const SimulationMinimumEventInterval = 1000; const MaximumSimulationSteps = 250000; +const MaximumSimulationSameTimePasses = 100; function GetSimulationTime( @@ -410,31 +411,47 @@ function AdvanceGameSimulation( ) { Result.Steps = Step + 1; - const MutationResult = - CheckAllGardenMutationsAtTime( - SaveData, - CurrentTime - ); - - MergeSimulationMutationResult( - Result, - MutationResult - ); - - if ( - typeof ProcessFunctionalEffectsAtTime === - "function" + for ( + let SameTimePass = 0; + SameTimePass < + MaximumSimulationSameTimePasses; + SameTimePass++ ) { - const FunctionalResult = - ProcessFunctionalEffectsAtTime( + const MutationResult = + CheckAllGardenMutationsAtTime( SaveData, CurrentTime ); - MergeSimulationFunctionalResult( + MergeSimulationMutationResult( Result, - FunctionalResult + MutationResult ); + + let FunctionalResult = null; + + if ( + typeof ProcessFunctionalEffectsAtTime === + "function" + ) { + FunctionalResult = + ProcessFunctionalEffectsAtTime( + SaveData, + CurrentTime + ); + + MergeSimulationFunctionalResult( + Result, + FunctionalResult + ); + } + + if ( + MutationResult.Changed !== true && + FunctionalResult?.Changed !== true + ) { + break; + } } if (CurrentTime >= SafeTargetTime) {