mirror of
https://git.srhtnin.garden/sarah/srhtnin.github.io.git
synced 2026-10-13 18:38:11 +02:00
1372 lines
28 KiB
JavaScript
1372 lines
28 KiB
JavaScript
function IsAdminPlainObject(Value) {
|
|
return (
|
|
Value !== null &&
|
|
typeof Value === "object" &&
|
|
!Array.isArray(Value)
|
|
);
|
|
}
|
|
|
|
|
|
function ValidateAdminInteger(
|
|
Value,
|
|
Name,
|
|
Minimum = null
|
|
) {
|
|
if (
|
|
!Number.isInteger(Value) ||
|
|
(
|
|
Minimum !== null &&
|
|
Value < Minimum
|
|
)
|
|
) {
|
|
throw new Error(
|
|
Name + " must be " +
|
|
(
|
|
Minimum === null
|
|
? "an integer."
|
|
: "an integer of at least " +
|
|
Minimum + "."
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
function ValidateAdminKey(
|
|
Value,
|
|
Name,
|
|
MaximumLength = 64
|
|
) {
|
|
if (
|
|
typeof Value !== "string" ||
|
|
Value.length === 0
|
|
) {
|
|
throw new Error(
|
|
Name + " cannot be empty."
|
|
);
|
|
}
|
|
|
|
if (
|
|
Value.length > MaximumLength ||
|
|
!/^[A-Za-z0-9_-]+$/.test(Value)
|
|
) {
|
|
throw new Error(
|
|
Name +
|
|
" may only contain letters, numbers, underscores and hyphens."
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
function ValidateAdminFunctionalEffects(
|
|
Effects
|
|
) {
|
|
const Functional =
|
|
Effects?.Functional;
|
|
|
|
if (Functional === undefined) {
|
|
return true;
|
|
}
|
|
|
|
if (!Array.isArray(Functional)) {
|
|
throw new Error(
|
|
"Effects.Functional must be an array."
|
|
);
|
|
}
|
|
|
|
if (Functional.length > 32) {
|
|
throw new Error(
|
|
"Effects.Functional cannot contain more than 32 effects."
|
|
);
|
|
}
|
|
|
|
const SeenKeys = new Set();
|
|
|
|
for (
|
|
let EffectIndex = 0;
|
|
EffectIndex < Functional.length;
|
|
EffectIndex++
|
|
) {
|
|
const Effect =
|
|
Functional[EffectIndex];
|
|
|
|
if (!IsAdminPlainObject(Effect)) {
|
|
throw new Error(
|
|
"Functional effect " +
|
|
(EffectIndex + 1) +
|
|
" must be an object."
|
|
);
|
|
}
|
|
|
|
const EffectType = String(
|
|
Effect.Type ?? ""
|
|
).trim();
|
|
|
|
ValidateAdminKey(
|
|
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 ?? ""
|
|
).trim();
|
|
|
|
ValidateAdminKey(
|
|
EffectKey,
|
|
"Functional effect key"
|
|
);
|
|
|
|
if (SeenKeys.has(EffectKey)) {
|
|
throw new Error(
|
|
"Functional effect keys must be unique within a plant."
|
|
);
|
|
}
|
|
|
|
SeenKeys.add(EffectKey);
|
|
}
|
|
|
|
for (const Field of [
|
|
"Area",
|
|
"Source",
|
|
"Target"
|
|
]) {
|
|
if (Effect[Field] === undefined) {
|
|
continue;
|
|
}
|
|
|
|
const Area = String(
|
|
Effect[Field]
|
|
);
|
|
|
|
if (
|
|
typeof FunctionalEffectAreas !==
|
|
"undefined" &&
|
|
!FunctionalEffectAreas.includes(
|
|
Area
|
|
)
|
|
) {
|
|
throw new Error(
|
|
"Functional effect " +
|
|
Field.toLowerCase() +
|
|
" must be Front, Behind, Cardinal, Adjacent or Farm."
|
|
);
|
|
}
|
|
}
|
|
|
|
if (Effect.Cooldown !== undefined) {
|
|
ValidateAdminInteger(
|
|
Number(
|
|
Effect.Cooldown
|
|
),
|
|
"Functional effect cooldown",
|
|
typeof FunctionalEffectMinimumCooldown !==
|
|
"undefined"
|
|
? FunctionalEffectMinimumCooldown
|
|
: 1000
|
|
);
|
|
}
|
|
|
|
if (Effect.Amount !== undefined) {
|
|
const Amount = Number(
|
|
Effect.Amount
|
|
);
|
|
|
|
if (!Number.isFinite(Amount)) {
|
|
throw new Error(
|
|
"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."
|
|
);
|
|
}
|
|
}
|
|
|
|
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 (
|
|
typeof IsFunctionalEffectActive ===
|
|
"function" &&
|
|
IsFunctionalEffectActive(
|
|
Effect
|
|
) &&
|
|
Effect.Cooldown === undefined
|
|
) {
|
|
throw new Error(
|
|
"Active functional effects need a cooldown."
|
|
);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
function ValidateAdminPlant(
|
|
Plant,
|
|
Catalogue,
|
|
EditingKey = null
|
|
) {
|
|
if (!IsAdminPlainObject(Plant)) {
|
|
throw new Error(
|
|
"Plant data is invalid."
|
|
);
|
|
}
|
|
|
|
ValidateAdminInteger(
|
|
Number(Plant.Id),
|
|
"ID",
|
|
1
|
|
);
|
|
|
|
const PlantKey =
|
|
String(
|
|
Plant.PlantKey ?? ""
|
|
).trim();
|
|
|
|
ValidateAdminKey(
|
|
PlantKey,
|
|
"Plant Key"
|
|
);
|
|
|
|
const ExistingAtId =
|
|
Object.entries(
|
|
Catalogue ?? {}
|
|
).find(
|
|
([, Candidate]) =>
|
|
Number(Candidate?.Id) ===
|
|
Number(Plant.Id)
|
|
);
|
|
|
|
if (
|
|
ExistingAtId !== undefined &&
|
|
ExistingAtId[0] !== PlantKey
|
|
) {
|
|
throw new Error(
|
|
"ID " + Plant.Id +
|
|
" already belongs to " +
|
|
ExistingAtId[0] + "."
|
|
);
|
|
}
|
|
|
|
if (
|
|
EditingKey !== null &&
|
|
EditingKey !== PlantKey
|
|
) {
|
|
throw new Error(
|
|
"An existing Plant Key cannot be changed."
|
|
);
|
|
}
|
|
|
|
if (
|
|
EditingKey !== null &&
|
|
Number(
|
|
Catalogue?.[EditingKey]?.Id
|
|
) !== Number(Plant.Id)
|
|
) {
|
|
throw new Error(
|
|
"An existing plant ID cannot be changed."
|
|
);
|
|
}
|
|
|
|
if (
|
|
EditingKey === null &&
|
|
Catalogue?.[PlantKey] !== undefined
|
|
) {
|
|
throw new Error(
|
|
"Plant Key " + PlantKey +
|
|
" already exists."
|
|
);
|
|
}
|
|
|
|
const Name =
|
|
String(
|
|
Plant.Name ?? ""
|
|
).trim();
|
|
|
|
if (
|
|
Name.length === 0 ||
|
|
Name.length > 64
|
|
) {
|
|
throw new Error(
|
|
"Name must contain between 1 and 64 characters."
|
|
);
|
|
}
|
|
|
|
if (
|
|
typeof Plant.Description !==
|
|
"string"
|
|
) {
|
|
throw new Error(
|
|
"Description must be text."
|
|
);
|
|
}
|
|
|
|
if (!Array.isArray(Plant.Tags)) {
|
|
throw new Error(
|
|
"Tags must be an array."
|
|
);
|
|
}
|
|
|
|
for (const Tag of Plant.Tags) {
|
|
if (
|
|
typeof Tag !== "string" ||
|
|
Tag.trim().length === 0 ||
|
|
Tag.trim().length > 64
|
|
) {
|
|
throw new Error(
|
|
"Tags contains an invalid tag."
|
|
);
|
|
}
|
|
}
|
|
|
|
ValidateAdminInteger(
|
|
Number(Plant.GrowthTime),
|
|
"Growth time",
|
|
0
|
|
);
|
|
|
|
const HarvestMultiplier =
|
|
Number(
|
|
Plant.HarvestMultiplier
|
|
);
|
|
|
|
if (
|
|
!Number.isFinite(
|
|
HarvestMultiplier
|
|
) ||
|
|
HarvestMultiplier < 0 ||
|
|
HarvestMultiplier > 9999.9999
|
|
) {
|
|
throw new Error(
|
|
"Harvest multiplier must be between 0 and 9999.9999."
|
|
);
|
|
}
|
|
|
|
if (!IsAdminPlainObject(Plant.Effects)) {
|
|
throw new Error(
|
|
"Effects JSON must be an object."
|
|
);
|
|
}
|
|
|
|
ValidateAdminFunctionalEffects(
|
|
Plant.Effects
|
|
);
|
|
|
|
if (
|
|
Plant.DirectionalSprites !== undefined &&
|
|
typeof Plant.DirectionalSprites !== "boolean"
|
|
) {
|
|
throw new Error(
|
|
"Directional sprites must be On or Off."
|
|
);
|
|
}
|
|
|
|
if (Plant.ShopPlant === true) {
|
|
ValidateAdminInteger(
|
|
Number(Plant.BaseCost),
|
|
"Base seed cost",
|
|
0
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
function GetAdminPlantKeyById(
|
|
PlantId,
|
|
PlantCatalogue
|
|
) {
|
|
return Object.entries(
|
|
PlantCatalogue ?? {}
|
|
).find(
|
|
([, Plant]) =>
|
|
Number(Plant?.Id) ===
|
|
Number(PlantId)
|
|
)?.[0] ?? null;
|
|
}
|
|
|
|
|
|
function ValidateAdminMutationMatrix(
|
|
Matrix,
|
|
Name,
|
|
Width = null,
|
|
Height = null
|
|
) {
|
|
if (
|
|
!Array.isArray(Matrix) ||
|
|
Matrix.length < 1 ||
|
|
Matrix.length > 7
|
|
) {
|
|
throw new Error(
|
|
Name +
|
|
" must contain between 1 and 7 rows."
|
|
);
|
|
}
|
|
|
|
const MatrixWidth =
|
|
Array.isArray(Matrix[0])
|
|
? Matrix[0].length
|
|
: 0;
|
|
|
|
if (
|
|
MatrixWidth < 1 ||
|
|
MatrixWidth > 7
|
|
) {
|
|
throw new Error(
|
|
Name +
|
|
" rows must contain between 1 and 7 cells."
|
|
);
|
|
}
|
|
|
|
for (const Row of Matrix) {
|
|
if (
|
|
!Array.isArray(Row) ||
|
|
Row.length !== MatrixWidth
|
|
) {
|
|
throw new Error(
|
|
Name +
|
|
" must be rectangular."
|
|
);
|
|
}
|
|
}
|
|
|
|
if (
|
|
Width !== null &&
|
|
MatrixWidth !== Width
|
|
) {
|
|
throw new Error(
|
|
Name +
|
|
" width must match the pattern."
|
|
);
|
|
}
|
|
|
|
if (
|
|
Height !== null &&
|
|
Matrix.length !== Height
|
|
) {
|
|
throw new Error(
|
|
Name +
|
|
" height must match the pattern."
|
|
);
|
|
}
|
|
|
|
return {
|
|
Width: MatrixWidth,
|
|
Height: Matrix.length
|
|
};
|
|
}
|
|
|
|
|
|
function ValidateAdminMutationTagList(
|
|
Value,
|
|
Name
|
|
) {
|
|
if (!Array.isArray(Value)) {
|
|
throw new Error(
|
|
Name + " must be an array."
|
|
);
|
|
}
|
|
|
|
for (const Tag of Value) {
|
|
if (
|
|
typeof Tag !== "string" ||
|
|
Tag.trim().length === 0 ||
|
|
Tag.trim().length > 64
|
|
) {
|
|
throw new Error(
|
|
Name +
|
|
" contains an invalid tag."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function ValidateAdminCaptureName(
|
|
Capture
|
|
) {
|
|
if (
|
|
typeof Capture !== "string" ||
|
|
!/^[A-Za-z][A-Za-z0-9_]*$/.test(
|
|
Capture
|
|
)
|
|
) {
|
|
throw new Error(
|
|
"Capture names must start with a letter and contain only letters, numbers and underscores."
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
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,
|
|
ListCount
|
|
) {
|
|
if (
|
|
Cell === null ||
|
|
Cell === "Any" ||
|
|
Cell === "Empty"
|
|
) {
|
|
return;
|
|
}
|
|
|
|
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
|
|
) {
|
|
throw new Error(
|
|
"Pattern references unknown plant: " +
|
|
Cell
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!IsAdminPlainObject(Cell)) {
|
|
throw new Error(
|
|
"Pattern contains an invalid matcher."
|
|
);
|
|
}
|
|
|
|
if (
|
|
Cell.Plant !== undefined &&
|
|
(
|
|
typeof Cell.Plant !==
|
|
"string" ||
|
|
PlantCatalogue?.[
|
|
Cell.Plant
|
|
] === undefined
|
|
)
|
|
) {
|
|
throw new Error(
|
|
"Pattern matcher references an unknown plant."
|
|
);
|
|
}
|
|
|
|
for (
|
|
const TagField
|
|
of [
|
|
"Tags",
|
|
"TagsAny",
|
|
"TagsNot"
|
|
]
|
|
) {
|
|
if (Cell[TagField] !== undefined) {
|
|
ValidateAdminMutationTagList(
|
|
Cell[TagField],
|
|
TagField
|
|
);
|
|
}
|
|
}
|
|
|
|
if (Cell.Capture !== undefined) {
|
|
ValidateAdminCaptureName(
|
|
Cell.Capture
|
|
);
|
|
|
|
Captures.add(
|
|
Cell.Capture
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
function ValidateAdminMutationResultCell(
|
|
Cell,
|
|
PlantCatalogue,
|
|
Captures,
|
|
ListCount
|
|
) {
|
|
if (
|
|
Cell === null ||
|
|
Cell === "Keep" ||
|
|
Cell === "Empty"
|
|
) {
|
|
return;
|
|
}
|
|
|
|
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) &&
|
|
typeof Cell.Plant === "string"
|
|
) {
|
|
PlantValue = Cell.Plant;
|
|
}
|
|
|
|
if (PlantValue === null) {
|
|
throw new Error(
|
|
"Result contains an invalid value."
|
|
);
|
|
}
|
|
|
|
if (PlantValue.startsWith("$")) {
|
|
const Capture =
|
|
PlantValue.slice(1);
|
|
|
|
if (!Captures.has(Capture)) {
|
|
throw new Error(
|
|
"Result references unknown capture: " +
|
|
Capture
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (
|
|
PlantCatalogue?.[
|
|
PlantValue
|
|
] === undefined
|
|
) {
|
|
throw new Error(
|
|
"Result references unknown plant: " +
|
|
PlantValue
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
function ValidateAdminMutationRelations(
|
|
Relations,
|
|
PlantCatalogue
|
|
) {
|
|
if (!IsAdminPlainObject(Relations)) {
|
|
throw new Error(
|
|
"Relations must be an object."
|
|
);
|
|
}
|
|
|
|
const ValidIds =
|
|
new Set(
|
|
Object.values(
|
|
PlantCatalogue ?? {}
|
|
).map(
|
|
Plant =>
|
|
Number(Plant.Id)
|
|
)
|
|
);
|
|
|
|
for (
|
|
const Name
|
|
of [
|
|
"PlantsUsed",
|
|
"PlantsCreated"
|
|
]
|
|
) {
|
|
const Values =
|
|
Relations[Name] ?? [];
|
|
|
|
if (!Array.isArray(Values)) {
|
|
throw new Error(
|
|
Name + " must be an array."
|
|
);
|
|
}
|
|
|
|
for (const PlantId of Values) {
|
|
if (
|
|
!Number.isInteger(
|
|
Number(PlantId)
|
|
) ||
|
|
!ValidIds.has(
|
|
Number(PlantId)
|
|
)
|
|
) {
|
|
throw new Error(
|
|
Name +
|
|
" references a plant that does not exist."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function ValidateAdminMutation(
|
|
Mutation,
|
|
MutationCatalogue,
|
|
PlantCatalogue,
|
|
EditingKey = null
|
|
) {
|
|
if (!IsAdminPlainObject(Mutation)) {
|
|
throw new Error(
|
|
"Mutation data is invalid."
|
|
);
|
|
}
|
|
|
|
ValidateAdminInteger(
|
|
Number(Mutation.Id),
|
|
"ID",
|
|
1
|
|
);
|
|
|
|
const MutationKey =
|
|
String(
|
|
Mutation.MutationKey ?? ""
|
|
).trim();
|
|
|
|
ValidateAdminKey(
|
|
MutationKey,
|
|
"Mutation Key"
|
|
);
|
|
|
|
const ExistingAtId =
|
|
Object.entries(
|
|
MutationCatalogue ?? {}
|
|
).find(
|
|
([, Candidate]) =>
|
|
Number(Candidate?.Id) ===
|
|
Number(Mutation.Id)
|
|
);
|
|
|
|
if (
|
|
ExistingAtId !== undefined &&
|
|
ExistingAtId[0] !== MutationKey
|
|
) {
|
|
throw new Error(
|
|
"ID " + Mutation.Id +
|
|
" already belongs to " +
|
|
ExistingAtId[0] + "."
|
|
);
|
|
}
|
|
|
|
if (
|
|
EditingKey !== null &&
|
|
EditingKey !== MutationKey
|
|
) {
|
|
throw new Error(
|
|
"An existing Mutation Key cannot be changed."
|
|
);
|
|
}
|
|
|
|
if (
|
|
EditingKey !== null &&
|
|
Number(
|
|
MutationCatalogue
|
|
?.[EditingKey]?.Id
|
|
) !== Number(Mutation.Id)
|
|
) {
|
|
throw new Error(
|
|
"An existing mutation ID cannot be changed."
|
|
);
|
|
}
|
|
|
|
if (
|
|
EditingKey === null &&
|
|
MutationCatalogue?.[
|
|
MutationKey
|
|
] !== undefined
|
|
) {
|
|
throw new Error(
|
|
"Mutation Key " +
|
|
MutationKey +
|
|
" already exists."
|
|
);
|
|
}
|
|
|
|
const Name =
|
|
String(
|
|
Mutation.Name ?? ""
|
|
).trim();
|
|
|
|
if (
|
|
Name.length === 0 ||
|
|
Name.length > 128
|
|
) {
|
|
throw new Error(
|
|
"Name must contain between 1 and 128 characters."
|
|
);
|
|
}
|
|
|
|
if (
|
|
typeof Mutation.Description !==
|
|
"string"
|
|
) {
|
|
throw new Error(
|
|
"Description must be text."
|
|
);
|
|
}
|
|
|
|
if (
|
|
Mutation.Hint !== null &&
|
|
Mutation.Hint !== undefined &&
|
|
typeof Mutation.Hint !== "string"
|
|
) {
|
|
throw new Error(
|
|
"Hint must be text."
|
|
);
|
|
}
|
|
|
|
ValidateAdminInteger(
|
|
Number(Mutation.Priority),
|
|
"Priority"
|
|
);
|
|
|
|
const Chance =
|
|
Number(
|
|
Mutation.Chance
|
|
);
|
|
|
|
if (
|
|
!Number.isFinite(Chance) ||
|
|
Chance < 0 ||
|
|
Chance > 1
|
|
) {
|
|
throw new Error(
|
|
"Chance must be between 0 and 100%."
|
|
);
|
|
}
|
|
|
|
ValidateAdminInteger(
|
|
Number(Mutation.Cooldown),
|
|
"Cooldown",
|
|
0
|
|
);
|
|
|
|
if (
|
|
Mutation.Rotation !== "None" &&
|
|
Mutation.Rotation !== "Any"
|
|
) {
|
|
throw new Error(
|
|
"Rotation must be None or Any."
|
|
);
|
|
}
|
|
|
|
ValidateAdminMutationLists(
|
|
Mutation.Lists ?? [],
|
|
PlantCatalogue
|
|
);
|
|
|
|
const ListCount =
|
|
(Mutation.Lists ?? []).length;
|
|
|
|
const PatternSize =
|
|
ValidateAdminMutationMatrix(
|
|
Mutation.Pattern,
|
|
"Pattern"
|
|
);
|
|
|
|
const Captures =
|
|
new Set();
|
|
|
|
for (const Row of Mutation.Pattern) {
|
|
for (const Cell of Row) {
|
|
ValidateAdminMutationPatternCell(
|
|
Cell,
|
|
PlantCatalogue,
|
|
Captures,
|
|
ListCount
|
|
);
|
|
}
|
|
}
|
|
|
|
ValidateAdminMutationOnceListCapacity(
|
|
Mutation.Lists ?? [],
|
|
Mutation.Pattern
|
|
);
|
|
|
|
ValidateAdminMutationMatrix(
|
|
Mutation.Success,
|
|
"Success result",
|
|
PatternSize.Width,
|
|
PatternSize.Height
|
|
);
|
|
|
|
for (const Row of Mutation.Success) {
|
|
for (const Cell of Row) {
|
|
ValidateAdminMutationResultCell(
|
|
Cell,
|
|
PlantCatalogue,
|
|
Captures,
|
|
ListCount
|
|
);
|
|
}
|
|
}
|
|
|
|
ValidateAdminMutationResultListReferences(
|
|
Mutation.Success,
|
|
Mutation.Pattern,
|
|
"Success result"
|
|
);
|
|
|
|
if (
|
|
Mutation.Failure !== "Keep" &&
|
|
Mutation.Failure !== "Clear"
|
|
) {
|
|
ValidateAdminMutationMatrix(
|
|
Mutation.Failure,
|
|
"Failure result",
|
|
PatternSize.Width,
|
|
PatternSize.Height
|
|
);
|
|
|
|
for (const Row of Mutation.Failure) {
|
|
for (const Cell of Row) {
|
|
ValidateAdminMutationResultCell(
|
|
Cell,
|
|
PlantCatalogue,
|
|
Captures,
|
|
ListCount
|
|
);
|
|
}
|
|
}
|
|
|
|
ValidateAdminMutationResultListReferences(
|
|
Mutation.Failure,
|
|
Mutation.Pattern,
|
|
"Failure result"
|
|
);
|
|
}
|
|
|
|
ValidateAdminMutationRelations(
|
|
Mutation.Relations ?? {},
|
|
PlantCatalogue
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
function DownloadAdminJson(
|
|
FileName,
|
|
Data
|
|
) {
|
|
const BlobData =
|
|
new Blob(
|
|
[
|
|
JSON.stringify(
|
|
Data,
|
|
null,
|
|
4
|
|
) + "\n"
|
|
],
|
|
{
|
|
type: "application/json"
|
|
}
|
|
);
|
|
|
|
const Url =
|
|
URL.createObjectURL(
|
|
BlobData
|
|
);
|
|
|
|
const Link =
|
|
document.createElement("a");
|
|
|
|
Link.href = Url;
|
|
Link.download = FileName;
|
|
Link.click();
|
|
|
|
URL.revokeObjectURL(Url);
|
|
}
|
|
|
|
|
|
function ReadAdminJsonFile(File) {
|
|
return new Promise(
|
|
(Resolve, Reject) => {
|
|
const Reader =
|
|
new FileReader();
|
|
|
|
Reader.onload = () => {
|
|
try {
|
|
Resolve(
|
|
JSON.parse(
|
|
String(
|
|
Reader.result ?? ""
|
|
)
|
|
)
|
|
);
|
|
} catch (Error) {
|
|
Reject(
|
|
new Error(
|
|
"The selected file is not valid JSON."
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
Reader.onerror = () => {
|
|
Reject(
|
|
new Error(
|
|
"Couldn't read the selected JSON file."
|
|
)
|
|
);
|
|
};
|
|
|
|
Reader.readAsText(File);
|
|
}
|
|
);
|
|
}
|