Files

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);
}
);
}