Added the actual functional effects, not just the base.

This commit is contained in:
sarah committed 2026-09-05 17:05:54 +02:00
1 parent 9de8af0d2b
commit 1452a4a96d
6 files changed
+1102 -88

No files matched your search

+52 -11
View File
@@ -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 (
+861 -2
View File
@@ -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,
+64 -56
View File
@@ -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."
);
+79 -1
View File
@@ -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
+11
View File
@@ -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 !==
+35 -18
View File
@@ -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) {