mirror of
https://git.srhtnin.garden/sarah/srhtnin.github.io.git
synced 2026-10-13 18:38:11 +02:00
2199 lines
42 KiB
JavaScript
2199 lines
42 KiB
JavaScript
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
|
|
) {
|
|
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)
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
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 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,
|
|
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;
|
|
}
|