Added lists to mutations for better recipes (more forgiving).

This commit is contained in:
sarah committed 2026-09-07 15:22:42 +02:00
1 parent d6fa5572ed
commit d9bdd74b02
11 files changed
+2249 -21

No files matched your search

+128
View File
@@ -744,6 +744,10 @@ function NormalizeAdminTransferMutation(
AllowImmature:
Imported.AllowImmature ===
true,
Lists:
CloneAdminTransferValue(
Imported.Lists ?? []
),
Pattern:
CloneAdminTransferValue(
Imported.Pattern
@@ -1908,6 +1912,10 @@ function GetAdminTransferPortableMutation(
AllowImmature:
Mutation.AllowImmature ===
true,
Lists:
CloneAdminTransferValue(
Mutation.Lists ?? []
),
Pattern:
CloneAdminTransferValue(
Mutation.Pattern
@@ -2638,10 +2646,106 @@ function CreateAdminTransferMutationRecipe(
Flow
);
const Lists =
CreateAdminTransferMutationLists(
Mutation.Lists,
PlantCatalogue
);
if (Lists !== null) {
Section.appendChild(Lists);
}
return Section;
}
function CreateAdminTransferMutationLists(
Lists,
PlantCatalogue
) {
if (
!Array.isArray(Lists) ||
Lists.length === 0
) {
return null;
}
const Container =
document.createElement("div");
Container.className =
"MutationRecipeLists";
const Heading =
document.createElement("h4");
Heading.textContent = "Lists";
const OrderedList =
document.createElement("ol");
for (const List of Lists) {
const ListItem =
document.createElement("li");
const Mode =
document.createElement("strong");
Mode.textContent =
List?.Mode === "Any"
? "Any: "
: "Once: ";
const Labels =
Array.isArray(List?.Items)
? List.Items.map(
Item => {
if (Item?.Type === "Plant") {
return PlantCatalogue?.[
Item.Value
]?.Name ??
Item.Value ??
"Unknown plant";
}
if (Item?.Type === "Tag") {
return "Tag: " +
(Item.Value ?? "");
}
if (Item?.Type === "Empty") {
return "Empty";
}
return "Any";
}
)
: [];
ListItem.append(
Mode,
document.createTextNode(
Labels.length > 0
? Labels.join(", ")
: "Empty list"
)
);
OrderedList.appendChild(
ListItem
);
}
Container.append(
Heading,
OrderedList
);
return Container;
}
function CreateAdminTransferMutationRecipePanel(
Heading,
Matrix,
@@ -2746,6 +2850,18 @@ function CreateAdminTransferMutationCell(
);
}
const ListMatch =
/^List:([1-9][0-9]*)$/.exec(
PlantValue
);
if (ListMatch !== null) {
return CreateAdminTransferTextCell(
"List " + ListMatch[1],
"MutationRecipeMatcher"
);
}
return CreateAdminTransferPlantCell(
PlantValue,
PlantCatalogue
@@ -2770,6 +2886,18 @@ function CreateAdminTransferMutationCell(
}
if (typeof Value === "string") {
const ListMatch =
/^List:([1-9][0-9]*)$/.exec(
Value
);
if (ListMatch !== null) {
return CreateAdminTransferTextCell(
"List " + ListMatch[1],
"MutationRecipeMatcher"
);
}
return CreateAdminTransferPlantCell(
Value,
PlantCatalogue
+21
View File
@@ -161,6 +161,9 @@ function UpdateAdminMassMutationFieldStates() {
AllowImmature: [
"AdminMassMutationAllowImmature"
],
Lists: [
"AdminMassMutationLists"
],
Pattern: [
"AdminMassMutationPattern"
],
@@ -399,6 +402,18 @@ function GetAdminMassMutationPatch() {
).value === "true";
}
if (
AdminMassMutationFieldEnabled(
"Lists"
)
) {
Patch.Lists =
ParseAdminMassMutationJson(
"AdminMassMutationLists",
"Lists JSON"
);
}
if (
AdminMassMutationFieldEnabled(
"Pattern"
@@ -489,6 +504,12 @@ function BuildAdminMassMutationPayload(
Source.Rotation ?? "None",
AllowImmature:
Source.AllowImmature === true,
Lists:
JSON.parse(
JSON.stringify(
Source.Lists ?? []
)
),
Pattern:
JSON.parse(
JSON.stringify(
File diff suppressed because it is too large. Load diff
+34
View File
@@ -275,6 +275,40 @@ function GetAdminOverviewPatternPlantKeys(
}
for (const Cell of Row) {
if (
typeof Cell === "string"
) {
const ListMatch =
/^List:([1-9][0-9]*)$/.exec(
Cell
);
if (ListMatch !== null) {
const List =
Mutation.Lists?.[
Number(ListMatch[1]) - 1
];
for (
const Item
of List?.Items ?? []
) {
if (
Item?.Type === "Plant" &&
AdminOverviewPlants[
Item.Value
] !== undefined
) {
PlantKeys.add(
Item.Value
);
}
}
continue;
}
}
if (
typeof Cell === "string" &&
AdminOverviewPlants[
+355 -5
View File
@@ -544,10 +544,293 @@ function ValidateAdminCaptureName(
}
function GetAdminValidationListReference(
Value
) {
if (typeof Value !== "string") {
return null;
}
const Match =
/^List:([1-9][0-9]*)$/.exec(
Value
);
return Match === null
? null
: Number(Match[1]);
}
function ValidateAdminMutationLists(
Lists,
PlantCatalogue
) {
if (!Array.isArray(Lists)) {
throw new Error(
"Lists must be an array."
);
}
if (Lists.length > 64) {
throw new Error(
"A mutation can contain at most 64 lists."
);
}
const ValidTags =
new Set(
Object.values(
PlantCatalogue ?? {}
).flatMap(
Plant =>
Array.isArray(Plant?.Tags)
? Plant.Tags
: []
).filter(
Tag =>
typeof Tag === "string" &&
Tag.trim() !== ""
).map(
Tag => Tag.trim()
)
);
for (
let ListIndex = 0;
ListIndex < Lists.length;
ListIndex++
) {
const List = Lists[ListIndex];
const ListNumber = ListIndex + 1;
if (!IsAdminPlainObject(List)) {
throw new Error(
"List " +
ListNumber +
" is invalid."
);
}
if (
List.Mode !== "Once" &&
List.Mode !== "Any"
) {
throw new Error(
"List " +
ListNumber +
" mode must be Once or Any."
);
}
if (
!Array.isArray(List.Items) ||
List.Items.length < 1 ||
List.Items.length > 64
) {
throw new Error(
"List " +
ListNumber +
" must contain between 1 and 64 items."
);
}
for (
let ItemIndex = 0;
ItemIndex < List.Items.length;
ItemIndex++
) {
const Item =
List.Items[ItemIndex];
const ItemName =
"List " +
ListNumber +
" item " +
(ItemIndex + 1);
if (!IsAdminPlainObject(Item)) {
throw new Error(
ItemName +
" is invalid."
);
}
if (
![
"Any",
"Empty",
"Plant",
"Tag"
].includes(Item.Type)
) {
throw new Error(
ItemName +
" has an invalid type."
);
}
if (Item.Type === "Plant") {
if (
typeof Item.Value !==
"string" ||
PlantCatalogue?.[
Item.Value
] === undefined
) {
throw new Error(
ItemName +
" references an unknown plant."
);
}
}
if (Item.Type === "Tag") {
const Tag =
typeof Item.Value ===
"string"
? Item.Value.trim()
: "";
if (
Tag.length === 0 ||
Tag.length > 64
) {
throw new Error(
ItemName +
" contains an invalid tag."
);
}
if (!ValidTags.has(Tag)) {
throw new Error(
ItemName +
" references a tag that no plant has: " +
Tag
);
}
}
}
}
}
function GetAdminMutationListReferenceCounts(
Matrix
) {
const Counts = new Map();
for (const Row of Matrix ?? []) {
if (!Array.isArray(Row)) {
continue;
}
for (const Cell of Row) {
const ListNumber =
GetAdminValidationListReference(
Cell
);
if (ListNumber === null) {
continue;
}
Counts.set(
ListNumber,
(Counts.get(ListNumber) ?? 0) + 1
);
}
}
return Counts;
}
function ValidateAdminMutationResultListReferences(
Result,
Pattern,
Name
) {
const PatternCounts =
GetAdminMutationListReferenceCounts(
Pattern
);
const ResultCounts =
GetAdminMutationListReferenceCounts(
Result
);
for (
const [ListNumber, Count]
of ResultCounts
) {
const PatternCount =
PatternCounts.get(ListNumber) ?? 0;
if (Count > PatternCount) {
throw new Error(
Name +
" uses List:" +
ListNumber +
" " +
Count +
" times, but the pattern only matches it " +
PatternCount +
" times."
);
}
}
}
function ValidateAdminMutationOnceListCapacity(
Lists,
Pattern
) {
const PatternCounts =
GetAdminMutationListReferenceCounts(
Pattern
);
for (
let ListIndex = 0;
ListIndex < Lists.length;
ListIndex++
) {
const List = Lists[ListIndex];
if (List?.Mode !== "Once") {
continue;
}
const ListNumber = ListIndex + 1;
const ReferenceCount =
PatternCounts.get(ListNumber) ?? 0;
if (
ReferenceCount >
(List.Items?.length ?? 0)
) {
throw new Error(
"List:" +
ListNumber +
" is Once and is referenced " +
ReferenceCount +
" times, but only has " +
(List.Items?.length ?? 0) +
" items."
);
}
}
}
function ValidateAdminMutationPatternCell(
Cell,
PlantCatalogue,
Captures
Captures,
ListCount
) {
if (
Cell === null ||
@@ -558,6 +841,25 @@ function ValidateAdminMutationPatternCell(
}
if (typeof Cell === "string") {
const ListNumber =
GetAdminValidationListReference(
Cell
);
if (ListNumber !== null) {
if (ListNumber > ListCount) {
throw new Error(
"Pattern references list " +
ListNumber +
", but only " +
ListCount +
" lists exist."
);
}
return;
}
if (
PlantCatalogue?.[Cell] ===
undefined
@@ -623,7 +925,8 @@ function ValidateAdminMutationPatternCell(
function ValidateAdminMutationResultCell(
Cell,
PlantCatalogue,
Captures
Captures,
ListCount
) {
if (
Cell === null ||
@@ -636,6 +939,25 @@ function ValidateAdminMutationResultCell(
let PlantValue = null;
if (typeof Cell === "string") {
const ListNumber =
GetAdminValidationListReference(
Cell
);
if (ListNumber !== null) {
if (ListNumber > ListCount) {
throw new Error(
"Result references list " +
ListNumber +
", but only " +
ListCount +
" lists exist."
);
}
return;
}
PlantValue = Cell;
} else if (
IsAdminPlainObject(Cell) &&
@@ -882,6 +1204,14 @@ function ValidateAdminMutation(
);
}
ValidateAdminMutationLists(
Mutation.Lists ?? [],
PlantCatalogue
);
const ListCount =
(Mutation.Lists ?? []).length;
const PatternSize =
ValidateAdminMutationMatrix(
Mutation.Pattern,
@@ -896,11 +1226,17 @@ function ValidateAdminMutation(
ValidateAdminMutationPatternCell(
Cell,
PlantCatalogue,
Captures
Captures,
ListCount
);
}
}
ValidateAdminMutationOnceListCapacity(
Mutation.Lists ?? [],
Mutation.Pattern
);
ValidateAdminMutationMatrix(
Mutation.Success,
"Success result",
@@ -913,11 +1249,18 @@ function ValidateAdminMutation(
ValidateAdminMutationResultCell(
Cell,
PlantCatalogue,
Captures
Captures,
ListCount
);
}
}
ValidateAdminMutationResultListReferences(
Mutation.Success,
Mutation.Pattern,
"Success result"
);
if (
Mutation.Failure !== "Keep" &&
Mutation.Failure !== "Clear"
@@ -934,10 +1277,17 @@ function ValidateAdminMutation(
ValidateAdminMutationResultCell(
Cell,
PlantCatalogue,
Captures
Captures,
ListCount
);
}
}
ValidateAdminMutationResultListReferences(
Mutation.Failure,
Mutation.Pattern,
"Failure result"
);
}
ValidateAdminMutationRelations(
+253 -8
View File
@@ -458,17 +458,142 @@ function CanAttemptHintedMutation(
}
return Mutation.Pattern.every(
Row =>
Array.isArray(Row) &&
Row.every(
Matcher =>
IsHintMatcherAvailable(
Matcher,
const ListReferenceCounts = {};
for (const Row of Mutation.Pattern) {
if (!Array.isArray(Row)) {
return false;
}
for (const Matcher of Row) {
const ListNumber =
GetMutationDisplayListReference(
Matcher
);
if (ListNumber !== null) {
ListReferenceCounts[
ListNumber
] =
(ListReferenceCounts[
ListNumber
] ?? 0) + 1;
continue;
}
if (
!IsHintMatcherAvailable(
Matcher,
SaveData
)
) {
return false;
}
}
}
for (
const [ListNumberText, Count]
of Object.entries(
ListReferenceCounts
)
) {
const List =
Mutation.Lists?.[
Number(ListNumberText) - 1
];
if (
List === undefined ||
!Array.isArray(List.Items)
) {
return false;
}
const AvailableItems =
List.Items.filter(
Item =>
IsHintListItemAvailable(
Item,
SaveData
)
).length;
if (
AvailableItems === 0 ||
(
List.Mode === "Once" &&
AvailableItems < Count
)
);
) {
return false;
}
}
return true;
}
function IsHintListItemAvailable(
Item,
SaveData
) {
if (
Item?.Type === "Any" ||
Item?.Type === "Empty"
) {
return true;
}
if (Item?.Type === "Plant") {
const Plant =
Plants[Item.Value];
return (
Plant !== undefined &&
HasDiscoveredPlant(
SaveData,
Plant.Id
)
);
}
if (Item?.Type === "Tag") {
return Object.values(
Plants
).some(
Plant =>
HasDiscoveredPlant(
SaveData,
Plant.Id
) &&
Array.isArray(Plant.Tags) &&
Plant.Tags.includes(
Item.Value
)
);
}
return false;
}
function GetMutationDisplayListReference(
Value
) {
if (typeof Value !== "string") {
return null;
}
const Match =
/^List:([1-9][0-9]*)$/.exec(
Value
);
return Match === null
? null
: Number(Match[1]);
}
@@ -1059,11 +1184,107 @@ function CreateMutationRecipeFlow(
Flow
);
const Lists =
CreateMutationRecipeLists(
Mutation.Lists
);
if (Lists !== null) {
Section.appendChild(Lists);
}
return Section;
}
function CreateMutationRecipeLists(
Lists
) {
if (
!Array.isArray(Lists) ||
Lists.length === 0
) {
return null;
}
const Container =
document.createElement("div");
Container.className =
"MutationRecipeLists";
const Heading =
document.createElement("h4");
Heading.textContent = "Lists";
const OrderedList =
document.createElement("ol");
for (const List of Lists) {
const Item =
document.createElement("li");
const Mode =
document.createElement("strong");
Mode.textContent =
List?.Mode === "Any"
? "Any: "
: "Once: ";
Item.appendChild(Mode);
const Labels =
Array.isArray(List?.Items)
? List.Items.map(
GetMutationRecipeListItemLabel
)
: [];
Item.append(
document.createTextNode(
Labels.length > 0
? Labels.join(", ")
: "Empty list"
)
);
OrderedList.appendChild(Item);
}
Container.append(
Heading,
OrderedList
);
return Container;
}
function GetMutationRecipeListItemLabel(
Item
) {
if (Item?.Type === "Plant") {
return Plants[Item.Value]?.Name ??
Item.Value ??
"Unknown plant";
}
if (Item?.Type === "Tag") {
return "Tag: " +
(Item.Value ?? "");
}
if (Item?.Type === "Empty") {
return "Empty";
}
return "Any";
}
function CreateMutationRecipePanel(
Heading,
Matrix,
@@ -1191,6 +1412,18 @@ function CreateMutationPatternCell(
typeof Matcher ===
"string"
) {
const ListNumber =
GetMutationDisplayListReference(
Matcher
);
if (ListNumber !== null) {
return CreateMutationTextCell(
"List " + ListNumber,
"MutationRecipeMatcher"
);
}
return CreateMutationPlantCell(
Matcher
);
@@ -1320,6 +1553,18 @@ function CreateMutationResultCell(
);
}
const ListNumber =
GetMutationDisplayListReference(
Result
);
if (ListNumber !== null) {
return CreateMutationTextCell(
"List " + ListNumber,
"MutationRecipeMatcher"
);
}
return CreateMutationPlantCell(
Result
);
+437 -8
View File
@@ -355,6 +355,7 @@ function CreateMutationCandidate(
) {
const Captures = {};
const Cells = [];
const ListReferences = {};
for (
@@ -395,6 +396,24 @@ function CreateMutationCandidate(
PlotIndex
);
const ListNumber =
GetMutationListReference(
Matcher
);
if (ListNumber !== null) {
ListReferences[ListNumber] ??= [];
ListReferences[ListNumber].push({
PlotIndex: PlotIndex,
LocalX: LocalX,
LocalY: LocalY,
Plot: Plot
});
continue;
}
if (
!DoesPlotMatchMutationMatcher(
@@ -411,6 +430,18 @@ function CreateMutationCandidate(
}
const ListMatches =
MatchMutationLists(
ListReferences,
Mutation,
AtTime
);
if (ListMatches === null) {
return null;
}
const GardenIndex =
Math.max(
0,
@@ -464,6 +495,7 @@ function CreateMutationCandidate(
Cells: Cells,
Captures: Captures,
ListMatches: ListMatches,
Priority:
Number(
@@ -472,7 +504,8 @@ function CreateMutationCandidate(
Specificity:
GetMutationPatternSpecificity(
Orientation.Pattern
Orientation.Pattern,
Mutation
)
};
}
@@ -513,11 +546,274 @@ function DoesMutationCandidateStillMatch(
Candidate.Captures =
RefreshedCandidate.Captures;
Candidate.ListMatches =
RefreshedCandidate.ListMatches;
return true;
}
function GetMutationListReference(
Value
) {
if (typeof Value !== "string") {
return null;
}
const Match =
/^List:([1-9][0-9]*)$/.exec(
Value
);
return Match === null
? null
: Number(Match[1]);
}
function MatchMutationLists(
ListReferences,
Mutation,
AtTime
) {
const Matches = {};
const Lists =
Array.isArray(Mutation.Lists)
? Mutation.Lists
: [];
for (
const [ListNumberText, References]
of Object.entries(ListReferences)
) {
const ListNumber =
Number(ListNumberText);
const List =
Lists[ListNumber - 1];
if (
List === undefined ||
!Array.isArray(List.Items) ||
List.Items.length === 0
) {
return null;
}
const CandidateItems =
References.map(
Reference =>
List.Items.map(
(Item, ItemIndex) => ({
Item: Item,
ItemIndex: ItemIndex
})
).filter(
Candidate =>
DoesPlotMatchMutationListItem(
Reference.Plot,
Candidate.Item,
Mutation,
AtTime
)
)
);
if (
CandidateItems.some(
Items => Items.length === 0
)
) {
return null;
}
let Assignments;
if (List.Mode === "Once") {
Assignments =
MatchMutationListOnce(
CandidateItems
);
if (Assignments === null) {
return null;
}
} else {
Assignments =
CandidateItems.map(
Items => Items[0]
);
}
Matches[ListNumber] =
References.map(
(Reference, Index) => ({
PlotIndex:
Reference.PlotIndex,
LocalX:
Reference.LocalX,
LocalY:
Reference.LocalY,
Plot:
CloneMutationPlot(
Reference.Plot
),
ItemIndex:
Assignments[Index]
.ItemIndex
})
);
}
return Matches;
}
function MatchMutationListOnce(
CandidateItems
) {
const ItemToReference =
new Map();
const Assignments =
new Array(
CandidateItems.length
);
function TryAssign(
ReferenceIndex,
SeenItems
) {
for (
const Candidate
of CandidateItems[
ReferenceIndex
]
) {
if (
SeenItems.has(
Candidate.ItemIndex
)
) {
continue;
}
SeenItems.add(
Candidate.ItemIndex
);
const PreviousReference =
ItemToReference.get(
Candidate.ItemIndex
);
if (
PreviousReference === undefined ||
TryAssign(
PreviousReference,
SeenItems
)
) {
ItemToReference.set(
Candidate.ItemIndex,
ReferenceIndex
);
Assignments[
ReferenceIndex
] = Candidate;
return true;
}
}
return false;
}
for (
let ReferenceIndex = 0;
ReferenceIndex < CandidateItems.length;
ReferenceIndex++
) {
if (
!TryAssign(
ReferenceIndex,
new Set()
)
) {
return null;
}
}
return Assignments;
}
function DoesPlotMatchMutationListItem(
Plot,
Item,
Mutation,
AtTime
) {
if (
Item === null ||
typeof Item !== "object"
) {
return false;
}
if (Item.Type === "Any") {
return true;
}
if (Item.Type === "Empty") {
return Plot === null;
}
if (Plot === null) {
return false;
}
const Plant =
Plants[Plot.Plant];
if (Plant === undefined) {
return false;
}
if (
Mutation.AllowImmature !== true &&
!IsMutationPlantMature(
Plot,
Plant,
AtTime
)
) {
return false;
}
if (Item.Type === "Plant") {
return (
typeof Item.Value === "string" &&
Plot.Plant === Item.Value
);
}
if (Item.Type === "Tag") {
return (
typeof Item.Value === "string" &&
Array.isArray(Plant.Tags) &&
Plant.Tags.includes(
Item.Value
)
);
}
return false;
}
function DoesPlotMatchMutationMatcher(
Plot,
Matcher,
@@ -756,6 +1052,7 @@ function ApplyMutationResult(
let Changed = false;
const UsedListResultMatches = {};
for (
@@ -798,16 +1095,47 @@ function ApplyMutationResult(
);
const NewPlot =
CreateMutationResultPlot(
ResultValue,
Candidate.Captures,
AtTime
const ListNumber =
GetMutationListReference(
ResultValue
);
let NewPlot;
if (ListNumber !== null) {
const Match =
GetMutationResultListMatch(
Candidate,
ListNumber,
LocalX,
LocalY,
UsedListResultMatches
);
if (Match === undefined) {
continue;
}
NewPlot =
CloneMutationPlot(
Match.Plot
);
} else {
NewPlot =
CreateMutationResultPlot(
ResultValue,
Candidate.Captures,
AtTime
);
}
if (
ResultValue === "Empty"
ResultValue === "Empty" ||
(
ListNumber !== null &&
NewPlot === null
)
) {
if (
SaveData.Garden.Plots[
@@ -881,6 +1209,50 @@ function ApplyMutationResult(
}
function GetMutationResultListMatch(
Candidate,
ListNumber,
LocalX,
LocalY,
UsedMatches
) {
const Matches =
Candidate.ListMatches?.[
ListNumber
] ?? [];
UsedMatches[ListNumber] ??=
new Set();
const Used =
UsedMatches[ListNumber];
let MatchIndex =
Matches.findIndex(
(Match, Index) =>
!Used.has(Index) &&
Match.LocalX === LocalX &&
Match.LocalY === LocalY
);
if (MatchIndex < 0) {
MatchIndex =
Matches.findIndex(
(Match, Index) =>
!Used.has(Index)
);
}
if (MatchIndex < 0) {
return undefined;
}
Used.add(MatchIndex);
return Matches[MatchIndex];
}
function CreateMutationResultPlot(
ResultValue,
Captures,
@@ -1066,7 +1438,8 @@ function CompareMutationCandidates(
function GetMutationPatternSpecificity(
Pattern
Pattern,
Mutation = null
) {
let Specificity = 0;
@@ -1097,6 +1470,22 @@ function GetMutationPatternSpecificity(
typeof Matcher ===
"string"
) {
const ListNumber =
GetMutationListReference(
Matcher
);
if (ListNumber !== null) {
Specificity +=
GetMutationListSpecificity(
Mutation?.Lists?.[
ListNumber - 1
]
);
continue;
}
Specificity += 8;
continue;
}
@@ -1141,6 +1530,46 @@ function GetMutationPatternSpecificity(
}
function GetMutationListSpecificity(
List
) {
if (
List === null ||
typeof List !== "object" ||
!Array.isArray(List.Items) ||
List.Items.length === 0
) {
return 0;
}
const ItemSpecificities =
List.Items.map(
Item => {
if (Item?.Type === "Empty") {
return 2;
}
if (Item?.Type === "Plant") {
return 8;
}
if (Item?.Type === "Tag") {
return 3;
}
return 0;
}
);
const Base = Math.min(
...ItemSpecificities
);
return Base +
(List.Mode === "Once" ? 1 : 0);
}
function GetMutationCandidateCooldownUntil(
SaveData,
Candidate