Files
srhtnin.github.io/Scripts/Garden/FunctionalEffects.js
T
2026-09-05 16:50:52 +02:00

1340 lines
25 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
) {
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;
}