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( ValidateAdminKey(
String( EffectType,
Effect.Type ?? ""
).trim(),
"Functional effect type" "Functional effect type"
); );
if (
typeof IsKnownFunctionalEffectType ===
"function" &&
!IsKnownFunctionalEffectType(
EffectType
)
) {
throw new Error(
"Unknown functional effect type: " +
EffectType +
"."
);
}
if (Effect.Key !== undefined) { if (Effect.Key !== undefined) {
const EffectKey = String( const EffectKey = String(
Effect.Key ?? "" Effect.Key ?? ""
@@ -175,16 +191,41 @@ function ValidateAdminFunctionalEffects(
"Functional effect amount must be a number." "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 ( for (const BooleanField of [
Effect.RequireMature !== undefined && "RequireMature",
typeof Effect.RequireMature !== "RequireMatureSource",
"boolean" "RandomSource",
) { "RandomTarget",
throw new Error( "ExcludeFunctional",
"Functional effect RequireMature must be On or Off." "ConsumeSeed"
); ]) {
if (
Effect[BooleanField] !== undefined &&
typeof Effect[BooleanField] !==
"boolean"
) {
throw new Error(
"Functional effect " +
BooleanField +
" must be On or Off."
);
}
} }
if ( if (
+861 -2
View File
@@ -180,8 +180,40 @@ function GetResolvedFunctionalEffectCooldown(
Effect, Effect,
AtTime AtTime
) { ) {
return GetFunctionalEffectCooldown( const BaseCooldown =
Effect 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() { function CreateFunctionalProcessingResult() {
return { return {
Attempted: false, Attempted: false,
+64 -56
View File
@@ -2185,28 +2185,27 @@ async function PlantSeed(
} }
if ( const PlantResult =
!TakeSeed( PlantGardenPlot(
GameSave, GameSave,
SelectedSeedId GameSave.ActiveGardenIndex,
) PlotIndex,
) { PlantKey,
ActionTime,
{
ConsumeSeed: true,
Rotation:
GardenPlotDefaultRotation
}
);
if (!PlantResult.Planted) {
EnsureSelectedSeed(); EnsureSelectedSeed();
RenderSeeds(); RenderSeeds();
return; return;
} }
GameSave.Garden.Plots[
PlotIndex
] = {
Plant: PlantKey,
PlantedAt: ActionTime,
Rotation: GardenPlotDefaultRotation
};
SetGardenMessage( SetGardenMessage(
"Planted " + "Planted " +
Plant.Name + Plant.Name +
@@ -2307,40 +2306,47 @@ async function PlantManySeeds() {
} }
if ( let PlantedCount = 0;
!TakeSeed(
GameSave,
SelectedSeedId,
PlantCount
)
) {
return;
}
const PlantedAt = ActionTime;
for ( for (
let Index = 0; let Index = 0;
Index < PlantCount; Index < PlantCount;
Index++ Index++
) { ) {
GameSave.Garden.Plots[ const PlantResult =
EmptyPlotIndexes[Index] PlantGardenPlot(
] = { GameSave,
Plant: PlantKey, GameSave.ActiveGardenIndex,
PlantedAt: PlantedAt, EmptyPlotIndexes[Index],
Rotation: GardenPlotDefaultRotation PlantKey,
}; ActionTime,
{
ConsumeSeed: true,
Rotation:
GardenPlotDefaultRotation
}
);
if (!PlantResult.Planted) {
break;
}
PlantedCount++;
}
if (PlantedCount <= 0) {
EnsureSelectedSeed();
RenderSeeds();
return;
} }
SetGardenMessage( SetGardenMessage(
"Magic trowel planted " + "Magic trowel planted " +
PlantCount.toLocaleString() + PlantedCount.toLocaleString() +
" " + " " +
Plant.Name + Plant.Name +
(PlantCount === 1 (PlantedCount === 1
? " seed." ? " seed."
: " seeds.") : " seeds.")
); );
@@ -2391,38 +2397,40 @@ async function HarvestPlant(
} }
const RewardAmount = const HarvestResult =
GetPlantHarvestReward( HarvestGardenPlot(
GameSave, GameSave,
Plant.Id GameSave.ActiveGardenIndex,
PlotIndex,
ActionTime
); );
if (RewardAmount === null) { if (!HarvestResult.Harvested) {
SetGardenMessage( if (HarvestResult.Disabled) {
"Couldn't calculate the harvest reward for " + SetGardenMessage(
Plant.Name + Plant.Name +
"." " can't be harvested right now."
); );
} else if (
HarvestResult.RewardUnavailable
) {
SetGardenMessage(
"Couldn't calculate the harvest reward for " +
Plant.Name +
"."
);
}
RenderGame();
return; return;
} }
GiveReward({
Currency: "Dew",
Amount: RewardAmount
});
GameSave.Garden.Plots[
PlotIndex
] = null;
SetGardenMessage( SetGardenMessage(
"Harvested " + "Harvested " +
Plant.Name + Plant.Name +
" for " + " for " +
RewardAmount + HarvestResult.RewardAmount +
" Dew." " Dew."
); );
+79 -1
View File
@@ -131,7 +131,7 @@ function CheckGardenMutations(
); );
const Chance = Math.max( const BaseChance = Math.max(
0, 0,
Math.min( Math.min(
Number( 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 = const Succeeded =
GetSimulationRandom( 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 { return {
Mutation: Mutation, Mutation: Mutation,
@@ -789,6 +850,23 @@ function ApplyMutationResult(
: "East"; : "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[ SaveData.Garden.Plots[
PlotIndex PlotIndex
+11
View File
@@ -288,6 +288,17 @@ function NormalizeGardenData(
? Plot.Rotation ? Plot.Rotation
: "East"; : "East";
if (
typeof Plot.AssignedSeed ===
"string" &&
Plot.AssignedSeed.trim().length > 0
) {
Plot.AssignedSeed =
Plot.AssignedSeed.trim();
} else {
delete Plot.AssignedSeed;
}
if ( if (
Plot.FunctionalCooldowns === null || Plot.FunctionalCooldowns === null ||
typeof Plot.FunctionalCooldowns !== typeof Plot.FunctionalCooldowns !==
+35 -18
View File
@@ -1,5 +1,6 @@
const SimulationMinimumEventInterval = 1000; const SimulationMinimumEventInterval = 1000;
const MaximumSimulationSteps = 250000; const MaximumSimulationSteps = 250000;
const MaximumSimulationSameTimePasses = 100;
function GetSimulationTime( function GetSimulationTime(
@@ -410,31 +411,47 @@ function AdvanceGameSimulation(
) { ) {
Result.Steps = Step + 1; Result.Steps = Step + 1;
const MutationResult = for (
CheckAllGardenMutationsAtTime( let SameTimePass = 0;
SaveData, SameTimePass <
CurrentTime MaximumSimulationSameTimePasses;
); SameTimePass++
MergeSimulationMutationResult(
Result,
MutationResult
);
if (
typeof ProcessFunctionalEffectsAtTime ===
"function"
) { ) {
const FunctionalResult = const MutationResult =
ProcessFunctionalEffectsAtTime( CheckAllGardenMutationsAtTime(
SaveData, SaveData,
CurrentTime CurrentTime
); );
MergeSimulationFunctionalResult( MergeSimulationMutationResult(
Result, 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) { if (CurrentTime >= SafeTargetTime) {