mirror of
https://git.srhtnin.garden/sarah/srhtnin.github.io.git
synced 2026-10-14 02:38:12 +02:00
Added more Admin QOL stuff. DW.
This commit is contained in:
1 parent
ab0d858fef
commit
24842c65fb
12 files changed
+4236
No files matched your search
@@ -0,0 +1,787 @@
|
||||
let AdminMassMutationCatalogue = {};
|
||||
let AdminMassMutationPlantCatalogue = {};
|
||||
let AdminMassMutationSavePending = false;
|
||||
|
||||
|
||||
async function AdminMassMutationRequest(
|
||||
Endpoint,
|
||||
Action,
|
||||
Data = {}
|
||||
) {
|
||||
const SaveKey =
|
||||
localStorage.getItem(
|
||||
SaveKeyName
|
||||
);
|
||||
|
||||
if (
|
||||
typeof SaveKey !== "string" ||
|
||||
!/^[0-9A-Fa-f]{64}$/.test(
|
||||
SaveKey
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"A valid Account Key is required."
|
||||
);
|
||||
}
|
||||
|
||||
const Response = await fetch(
|
||||
ApiUrl + "/" + Endpoint,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type":
|
||||
"application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
SaveKey: SaveKey,
|
||||
Action: Action,
|
||||
...Data
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const Result =
|
||||
await Response.json();
|
||||
|
||||
if (
|
||||
!Response.ok ||
|
||||
Result.Success !== true
|
||||
) {
|
||||
throw new Error(
|
||||
Result.Error ??
|
||||
"Mutation admin request failed."
|
||||
);
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
|
||||
function GetSortedAdminMassMutationKeys() {
|
||||
return Object.keys(
|
||||
AdminMassMutationCatalogue
|
||||
).sort(
|
||||
(A, B) =>
|
||||
Number(
|
||||
AdminMassMutationCatalogue[A]
|
||||
?.Id ?? 0
|
||||
) -
|
||||
Number(
|
||||
AdminMassMutationCatalogue[B]
|
||||
?.Id ?? 0
|
||||
) ||
|
||||
A.localeCompare(B)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function RenderAdminMassMutationSelection() {
|
||||
const Container =
|
||||
document.getElementById(
|
||||
"AdminMassMutationSelection"
|
||||
);
|
||||
|
||||
Container.replaceChildren();
|
||||
|
||||
for (
|
||||
const MutationKey
|
||||
of GetSortedAdminMassMutationKeys()
|
||||
) {
|
||||
const Mutation =
|
||||
AdminMassMutationCatalogue[
|
||||
MutationKey
|
||||
];
|
||||
|
||||
const Label =
|
||||
document.createElement("label");
|
||||
Label.className =
|
||||
"AdminPlantChecklistItem";
|
||||
|
||||
const Input =
|
||||
document.createElement("input");
|
||||
Input.type = "checkbox";
|
||||
Input.value = MutationKey;
|
||||
Input.dataset.adminMassTarget =
|
||||
"mutation";
|
||||
|
||||
const Text =
|
||||
document.createElement("span");
|
||||
Text.textContent =
|
||||
String(Mutation.Id).padStart(
|
||||
3,
|
||||
"0"
|
||||
) +
|
||||
" — " +
|
||||
(Mutation.Name ?? MutationKey) +
|
||||
(
|
||||
Mutation.Archived === true
|
||||
? " (archived)"
|
||||
: ""
|
||||
);
|
||||
|
||||
Label.append(Input, Text);
|
||||
Container.appendChild(Label);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function SetAdminMassMutationSelection(
|
||||
Checked
|
||||
) {
|
||||
for (
|
||||
const Input
|
||||
of document.querySelectorAll(
|
||||
'[data-admin-mass-target="mutation"]'
|
||||
)
|
||||
) {
|
||||
Input.checked = Checked;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function UpdateAdminMassMutationFieldStates() {
|
||||
const FieldMap = {
|
||||
Name: ["AdminMassMutationName"],
|
||||
Description: [
|
||||
"AdminMassMutationDescription"
|
||||
],
|
||||
Hint: ["AdminMassMutationHint"],
|
||||
Priority: [
|
||||
"AdminMassMutationPriority"
|
||||
],
|
||||
Chance: ["AdminMassMutationChance"],
|
||||
Cooldown: [
|
||||
"AdminMassMutationCooldownHours",
|
||||
"AdminMassMutationCooldownMinutes",
|
||||
"AdminMassMutationCooldownSeconds"
|
||||
],
|
||||
Rotation: [
|
||||
"AdminMassMutationRotation"
|
||||
],
|
||||
AllowImmature: [
|
||||
"AdminMassMutationAllowImmature"
|
||||
],
|
||||
Pattern: [
|
||||
"AdminMassMutationPattern"
|
||||
],
|
||||
Success: [
|
||||
"AdminMassMutationSuccess"
|
||||
],
|
||||
Failure: [
|
||||
"AdminMassMutationFailure"
|
||||
],
|
||||
PlantsUsed: [
|
||||
"AdminMassMutationPlantsUsed"
|
||||
],
|
||||
PlantsCreated: [
|
||||
"AdminMassMutationPlantsCreated"
|
||||
]
|
||||
};
|
||||
|
||||
for (
|
||||
const Apply
|
||||
of document.querySelectorAll(
|
||||
"[data-admin-mass-apply]"
|
||||
)
|
||||
) {
|
||||
const FieldName =
|
||||
Apply.dataset.adminMassApply;
|
||||
|
||||
for (
|
||||
const ElementId
|
||||
of FieldMap[FieldName] ?? []
|
||||
) {
|
||||
document.getElementById(
|
||||
ElementId
|
||||
).disabled = !Apply.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function AdminMassMutationFieldEnabled(
|
||||
FieldName
|
||||
) {
|
||||
return document.querySelector(
|
||||
'[data-admin-mass-apply="' +
|
||||
FieldName +
|
||||
'"]'
|
||||
)?.checked === true;
|
||||
}
|
||||
|
||||
|
||||
function GetAdminMassMutationSelectedKeys() {
|
||||
return [
|
||||
...document.querySelectorAll(
|
||||
'[data-admin-mass-target="mutation"]:checked'
|
||||
)
|
||||
].map(Input => Input.value);
|
||||
}
|
||||
|
||||
|
||||
function GetAdminMassMutationCooldown() {
|
||||
const Hours = Number(
|
||||
document.getElementById(
|
||||
"AdminMassMutationCooldownHours"
|
||||
).value
|
||||
);
|
||||
const Minutes = Number(
|
||||
document.getElementById(
|
||||
"AdminMassMutationCooldownMinutes"
|
||||
).value
|
||||
);
|
||||
const Seconds = Number(
|
||||
document.getElementById(
|
||||
"AdminMassMutationCooldownSeconds"
|
||||
).value
|
||||
);
|
||||
|
||||
if (
|
||||
!Number.isInteger(Hours) ||
|
||||
!Number.isInteger(Minutes) ||
|
||||
!Number.isInteger(Seconds) ||
|
||||
Hours < 0 ||
|
||||
Minutes < 0 ||
|
||||
Minutes > 59 ||
|
||||
Seconds < 0 ||
|
||||
Seconds > 59
|
||||
) {
|
||||
throw new Error(
|
||||
"Cooldown is invalid."
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
Hours * 3600000 +
|
||||
Minutes * 60000 +
|
||||
Seconds * 1000
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function ParseAdminMassMutationJson(
|
||||
ElementId,
|
||||
Name
|
||||
) {
|
||||
const Raw =
|
||||
document.getElementById(
|
||||
ElementId
|
||||
).value.trim();
|
||||
|
||||
try {
|
||||
return JSON.parse(Raw);
|
||||
} catch (Error) {
|
||||
throw new Error(
|
||||
Name + " is not valid JSON."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function ParseAdminMassMutationPlantKeys(
|
||||
ElementId,
|
||||
Name
|
||||
) {
|
||||
const Keys = [
|
||||
...new Set(
|
||||
document.getElementById(
|
||||
ElementId
|
||||
).value
|
||||
.split(",")
|
||||
.map(Value => Value.trim())
|
||||
.filter(Value => Value !== "")
|
||||
)
|
||||
];
|
||||
|
||||
return Keys.map(
|
||||
PlantKey => {
|
||||
const Plant =
|
||||
AdminMassMutationPlantCatalogue[
|
||||
PlantKey
|
||||
];
|
||||
|
||||
if (Plant === undefined) {
|
||||
throw new Error(
|
||||
Name +
|
||||
" references unknown Plant Key: " +
|
||||
PlantKey
|
||||
);
|
||||
}
|
||||
|
||||
return Number(Plant.Id);
|
||||
}
|
||||
).sort((A, B) => A - B);
|
||||
}
|
||||
|
||||
|
||||
function GetAdminMassMutationPatch() {
|
||||
const Patch = {};
|
||||
|
||||
if (AdminMassMutationFieldEnabled("Name")) {
|
||||
Patch.Name =
|
||||
document.getElementById(
|
||||
"AdminMassMutationName"
|
||||
).value.trim();
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"Description"
|
||||
)
|
||||
) {
|
||||
Patch.Description =
|
||||
document.getElementById(
|
||||
"AdminMassMutationDescription"
|
||||
).value.trim();
|
||||
}
|
||||
|
||||
if (AdminMassMutationFieldEnabled("Hint")) {
|
||||
Patch.Hint =
|
||||
document.getElementById(
|
||||
"AdminMassMutationHint"
|
||||
).value.trim();
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"Priority"
|
||||
)
|
||||
) {
|
||||
Patch.Priority =
|
||||
Number(
|
||||
document.getElementById(
|
||||
"AdminMassMutationPriority"
|
||||
).value
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"Chance"
|
||||
)
|
||||
) {
|
||||
Patch.Chance =
|
||||
Number(
|
||||
document.getElementById(
|
||||
"AdminMassMutationChance"
|
||||
).value
|
||||
) / 100;
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"Cooldown"
|
||||
)
|
||||
) {
|
||||
Patch.Cooldown =
|
||||
GetAdminMassMutationCooldown();
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"Rotation"
|
||||
)
|
||||
) {
|
||||
Patch.Rotation =
|
||||
document.getElementById(
|
||||
"AdminMassMutationRotation"
|
||||
).value;
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"AllowImmature"
|
||||
)
|
||||
) {
|
||||
Patch.AllowImmature =
|
||||
document.getElementById(
|
||||
"AdminMassMutationAllowImmature"
|
||||
).value === "true";
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"Pattern"
|
||||
)
|
||||
) {
|
||||
Patch.Pattern =
|
||||
ParseAdminMassMutationJson(
|
||||
"AdminMassMutationPattern",
|
||||
"Pattern JSON"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"Success"
|
||||
)
|
||||
) {
|
||||
Patch.Success =
|
||||
ParseAdminMassMutationJson(
|
||||
"AdminMassMutationSuccess",
|
||||
"Success result JSON"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"Failure"
|
||||
)
|
||||
) {
|
||||
Patch.Failure =
|
||||
ParseAdminMassMutationJson(
|
||||
"AdminMassMutationFailure",
|
||||
"Failure result JSON"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"PlantsUsed"
|
||||
)
|
||||
) {
|
||||
Patch.PlantsUsed =
|
||||
ParseAdminMassMutationPlantKeys(
|
||||
"AdminMassMutationPlantsUsed",
|
||||
"Plants used"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassMutationFieldEnabled(
|
||||
"PlantsCreated"
|
||||
)
|
||||
) {
|
||||
Patch.PlantsCreated =
|
||||
ParseAdminMassMutationPlantKeys(
|
||||
"AdminMassMutationPlantsCreated",
|
||||
"Plants created"
|
||||
);
|
||||
}
|
||||
|
||||
return Patch;
|
||||
}
|
||||
|
||||
|
||||
function BuildAdminMassMutationPayload(
|
||||
MutationKey,
|
||||
Patch
|
||||
) {
|
||||
const Source =
|
||||
AdminMassMutationCatalogue[
|
||||
MutationKey
|
||||
];
|
||||
|
||||
const Mutation = {
|
||||
Id: Number(Source.Id),
|
||||
MutationKey: MutationKey,
|
||||
Name: Source.Name ?? "",
|
||||
Description:
|
||||
Source.Description ?? "",
|
||||
Hint: Source.Hint ?? "",
|
||||
Priority:
|
||||
Number(Source.Priority ?? 0),
|
||||
Chance:
|
||||
Number(Source.Chance ?? 0),
|
||||
Cooldown:
|
||||
Number(Source.Cooldown ?? 0),
|
||||
Rotation:
|
||||
Source.Rotation ?? "None",
|
||||
AllowImmature:
|
||||
Source.AllowImmature === true,
|
||||
Pattern:
|
||||
JSON.parse(
|
||||
JSON.stringify(
|
||||
Source.Pattern ?? []
|
||||
)
|
||||
),
|
||||
Success:
|
||||
JSON.parse(
|
||||
JSON.stringify(
|
||||
Source.Success ?? []
|
||||
)
|
||||
),
|
||||
Failure:
|
||||
JSON.parse(
|
||||
JSON.stringify(
|
||||
Source.Failure ?? "Keep"
|
||||
)
|
||||
),
|
||||
Relations: {
|
||||
PlantsUsed: [
|
||||
...(Source.Relations
|
||||
?.PlantsUsed ?? [])
|
||||
],
|
||||
PlantsCreated: [
|
||||
...(Source.Relations
|
||||
?.PlantsCreated ?? [])
|
||||
]
|
||||
},
|
||||
IsNew: false
|
||||
};
|
||||
|
||||
for (
|
||||
const [Key, Value]
|
||||
of Object.entries(Patch)
|
||||
) {
|
||||
if (
|
||||
Key === "PlantsUsed" ||
|
||||
Key === "PlantsCreated"
|
||||
) {
|
||||
Mutation.Relations[Key] = [
|
||||
...Value
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
Mutation[Key] =
|
||||
Value !== null &&
|
||||
typeof Value === "object"
|
||||
? JSON.parse(
|
||||
JSON.stringify(Value)
|
||||
)
|
||||
: Value;
|
||||
}
|
||||
|
||||
return Mutation;
|
||||
}
|
||||
|
||||
|
||||
async function SaveAdminMassMutations(
|
||||
Event
|
||||
) {
|
||||
Event.preventDefault();
|
||||
|
||||
if (AdminMassMutationSavePending) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SelectedKeys =
|
||||
GetAdminMassMutationSelectedKeys();
|
||||
|
||||
if (SelectedKeys.length === 0) {
|
||||
SetAdminMassMutationMessage(
|
||||
"Mark at least one mutation first."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Patch;
|
||||
|
||||
try {
|
||||
Patch =
|
||||
GetAdminMassMutationPatch();
|
||||
} catch (Error) {
|
||||
SetAdminMassMutationMessage(
|
||||
Error.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(Patch).length === 0) {
|
||||
SetAdminMassMutationMessage(
|
||||
"Enable at least one field to update."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const Payloads = [];
|
||||
|
||||
try {
|
||||
for (
|
||||
const MutationKey
|
||||
of SelectedKeys
|
||||
) {
|
||||
const Mutation =
|
||||
BuildAdminMassMutationPayload(
|
||||
MutationKey,
|
||||
Patch
|
||||
);
|
||||
|
||||
ValidateAdminMutation(
|
||||
Mutation,
|
||||
AdminMassMutationCatalogue,
|
||||
AdminMassMutationPlantCatalogue,
|
||||
MutationKey
|
||||
);
|
||||
|
||||
Payloads.push(Mutation);
|
||||
}
|
||||
} catch (Error) {
|
||||
SetAdminMassMutationMessage(
|
||||
Error.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
AdminMassMutationSavePending = true;
|
||||
|
||||
const SaveButton =
|
||||
document.getElementById(
|
||||
"AdminMassMutationSaveButton"
|
||||
);
|
||||
|
||||
SaveButton.disabled = true;
|
||||
|
||||
let SavedCount = 0;
|
||||
|
||||
try {
|
||||
for (const Mutation of Payloads) {
|
||||
SetAdminMassMutationMessage(
|
||||
"Saving " +
|
||||
(SavedCount + 1) +
|
||||
" of " +
|
||||
Payloads.length +
|
||||
"..."
|
||||
);
|
||||
|
||||
await AdminMassMutationRequest(
|
||||
"AdminMutations.php",
|
||||
"Save",
|
||||
{Mutation: Mutation}
|
||||
);
|
||||
|
||||
SavedCount++;
|
||||
}
|
||||
|
||||
localStorage.removeItem(
|
||||
"SarahtoninGardenContent"
|
||||
);
|
||||
|
||||
await LoadAdminMassMutationData();
|
||||
|
||||
SetAdminMassMutationMessage(
|
||||
SavedCount === 1
|
||||
? "1 mutation updated."
|
||||
: SavedCount +
|
||||
" mutations updated."
|
||||
);
|
||||
} catch (Error) {
|
||||
SetAdminMassMutationMessage(
|
||||
"Saved " +
|
||||
SavedCount +
|
||||
" before stopping: " +
|
||||
(Error.message ??
|
||||
"Couldn't save mutations.")
|
||||
);
|
||||
} finally {
|
||||
AdminMassMutationSavePending = false;
|
||||
SaveButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function BindAdminMassMutationEditor() {
|
||||
document.getElementById(
|
||||
"AdminMassMutationSelectAll"
|
||||
).addEventListener(
|
||||
"click",
|
||||
() =>
|
||||
SetAdminMassMutationSelection(
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminMassMutationClear"
|
||||
).addEventListener(
|
||||
"click",
|
||||
() =>
|
||||
SetAdminMassMutationSelection(
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminMassMutationForm"
|
||||
).addEventListener(
|
||||
"submit",
|
||||
SaveAdminMassMutations
|
||||
);
|
||||
|
||||
for (
|
||||
const Apply
|
||||
of document.querySelectorAll(
|
||||
"[data-admin-mass-apply]"
|
||||
)
|
||||
) {
|
||||
Apply.addEventListener(
|
||||
"change",
|
||||
UpdateAdminMassMutationFieldStates
|
||||
);
|
||||
}
|
||||
|
||||
UpdateAdminMassMutationFieldStates();
|
||||
}
|
||||
|
||||
|
||||
async function LoadAdminMassMutationData() {
|
||||
const [MutationResult, PlantResult] =
|
||||
await Promise.all([
|
||||
AdminMassMutationRequest(
|
||||
"AdminMutations.php",
|
||||
"List"
|
||||
),
|
||||
AdminMassMutationRequest(
|
||||
"AdminPlants.php",
|
||||
"List"
|
||||
)
|
||||
]);
|
||||
|
||||
AdminMassMutationCatalogue =
|
||||
MutationResult.Mutations ?? {};
|
||||
AdminMassMutationPlantCatalogue =
|
||||
PlantResult.Plants ?? {};
|
||||
|
||||
RenderAdminMassMutationSelection();
|
||||
}
|
||||
|
||||
|
||||
function SetAdminMassMutationMessage(
|
||||
Message
|
||||
) {
|
||||
document.getElementById(
|
||||
"AdminMassMutationMessage"
|
||||
).textContent = Message;
|
||||
}
|
||||
|
||||
|
||||
async function StartAdminMassMutationEditor() {
|
||||
try {
|
||||
const Status =
|
||||
await GetAdminStatus();
|
||||
|
||||
if (
|
||||
Status.Success !== true ||
|
||||
Status.IsAdmin !== true
|
||||
) {
|
||||
SetAdminMassMutationMessage(
|
||||
"Admin access required."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
BindAdminMassMutationEditor();
|
||||
await LoadAdminMassMutationData();
|
||||
|
||||
SetAdminMassMutationMessage(
|
||||
"Mark mutations and enable the fields you want to change."
|
||||
);
|
||||
} catch (Error) {
|
||||
console.error(
|
||||
"Couldn't start Mass mutation editor:",
|
||||
Error
|
||||
);
|
||||
|
||||
SetAdminMassMutationMessage(
|
||||
Error.message ??
|
||||
"Couldn't load the Mass mutation editor."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
StartAdminMassMutationEditor
|
||||
);
|
||||
@@ -0,0 +1,645 @@
|
||||
let AdminMassPlantCatalogue = {};
|
||||
let AdminMassPlantSavePending = false;
|
||||
|
||||
|
||||
async function AdminMassPlantRequest(
|
||||
Action,
|
||||
Data = {}
|
||||
) {
|
||||
const SaveKey =
|
||||
localStorage.getItem(
|
||||
SaveKeyName
|
||||
);
|
||||
|
||||
if (
|
||||
typeof SaveKey !== "string" ||
|
||||
!/^[0-9A-Fa-f]{64}$/.test(
|
||||
SaveKey
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"A valid Account Key is required."
|
||||
);
|
||||
}
|
||||
|
||||
const Response = await fetch(
|
||||
ApiUrl + "/AdminPlants.php",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type":
|
||||
"application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
SaveKey: SaveKey,
|
||||
Action: Action,
|
||||
...Data
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const Result =
|
||||
await Response.json();
|
||||
|
||||
if (
|
||||
!Response.ok ||
|
||||
Result.Success !== true
|
||||
) {
|
||||
throw new Error(
|
||||
Result.Error ??
|
||||
"Plant admin request failed."
|
||||
);
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
|
||||
function GetSortedAdminMassPlantKeys() {
|
||||
return Object.keys(
|
||||
AdminMassPlantCatalogue
|
||||
).sort(
|
||||
(A, B) =>
|
||||
Number(
|
||||
AdminMassPlantCatalogue[A]
|
||||
?.Id ?? 0
|
||||
) -
|
||||
Number(
|
||||
AdminMassPlantCatalogue[B]
|
||||
?.Id ?? 0
|
||||
) ||
|
||||
A.localeCompare(B)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function RenderAdminMassPlantSelection() {
|
||||
const Container =
|
||||
document.getElementById(
|
||||
"AdminMassPlantSelection"
|
||||
);
|
||||
|
||||
Container.replaceChildren();
|
||||
|
||||
for (
|
||||
const PlantKey
|
||||
of GetSortedAdminMassPlantKeys()
|
||||
) {
|
||||
const Plant =
|
||||
AdminMassPlantCatalogue[
|
||||
PlantKey
|
||||
];
|
||||
|
||||
const Label =
|
||||
document.createElement("label");
|
||||
Label.className =
|
||||
"AdminPlantChecklistItem";
|
||||
|
||||
const Input =
|
||||
document.createElement("input");
|
||||
Input.type = "checkbox";
|
||||
Input.value = PlantKey;
|
||||
Input.dataset.adminMassTarget =
|
||||
"plant";
|
||||
|
||||
const Text =
|
||||
document.createElement("span");
|
||||
Text.textContent =
|
||||
String(Plant.Id).padStart(
|
||||
3,
|
||||
"0"
|
||||
) +
|
||||
" — " +
|
||||
(Plant.Name ?? PlantKey) +
|
||||
(
|
||||
Plant.Archived === true
|
||||
? " (archived)"
|
||||
: ""
|
||||
);
|
||||
|
||||
Label.append(
|
||||
Input,
|
||||
Text
|
||||
);
|
||||
Container.appendChild(Label);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function SetAdminMassPlantSelection(
|
||||
Checked
|
||||
) {
|
||||
for (
|
||||
const Input
|
||||
of document.querySelectorAll(
|
||||
'[data-admin-mass-target="plant"]'
|
||||
)
|
||||
) {
|
||||
Input.checked = Checked;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function UpdateAdminMassPlantFieldStates() {
|
||||
const FieldMap = {
|
||||
Name: [
|
||||
"AdminMassPlantName"
|
||||
],
|
||||
Description: [
|
||||
"AdminMassPlantDescription"
|
||||
],
|
||||
Tags: [
|
||||
"AdminMassPlantTags"
|
||||
],
|
||||
GrowthTime: [
|
||||
"AdminMassPlantGrowthHours",
|
||||
"AdminMassPlantGrowthMinutes",
|
||||
"AdminMassPlantGrowthSeconds"
|
||||
],
|
||||
HarvestMultiplier: [
|
||||
"AdminMassPlantHarvestMultiplier"
|
||||
],
|
||||
ShopPlant: [
|
||||
"AdminMassPlantShopPlant"
|
||||
],
|
||||
BaseCost: [
|
||||
"AdminMassPlantBaseCost"
|
||||
],
|
||||
Effects: [
|
||||
"AdminMassPlantEffects"
|
||||
]
|
||||
};
|
||||
|
||||
for (
|
||||
const Apply
|
||||
of document.querySelectorAll(
|
||||
"[data-admin-mass-apply]"
|
||||
)
|
||||
) {
|
||||
const FieldName =
|
||||
Apply.dataset.adminMassApply;
|
||||
|
||||
for (
|
||||
const ElementId
|
||||
of FieldMap[FieldName] ?? []
|
||||
) {
|
||||
document.getElementById(
|
||||
ElementId
|
||||
).disabled = !Apply.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function GetAdminMassPlantSelectedKeys() {
|
||||
return [
|
||||
...document.querySelectorAll(
|
||||
'[data-admin-mass-target="plant"]:checked'
|
||||
)
|
||||
].map(Input => Input.value);
|
||||
}
|
||||
|
||||
|
||||
function AdminMassPlantFieldEnabled(
|
||||
FieldName
|
||||
) {
|
||||
return document.querySelector(
|
||||
'[data-admin-mass-apply="' +
|
||||
FieldName +
|
||||
'"]'
|
||||
)?.checked === true;
|
||||
}
|
||||
|
||||
|
||||
function ParseAdminMassPlantTags() {
|
||||
return [
|
||||
...new Set(
|
||||
document.getElementById(
|
||||
"AdminMassPlantTags"
|
||||
).value
|
||||
.split(",")
|
||||
.map(Tag => Tag.trim())
|
||||
.filter(Tag => Tag !== "")
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
function GetAdminMassPlantGrowthTime() {
|
||||
const Hours = Number(
|
||||
document.getElementById(
|
||||
"AdminMassPlantGrowthHours"
|
||||
).value
|
||||
);
|
||||
const Minutes = Number(
|
||||
document.getElementById(
|
||||
"AdminMassPlantGrowthMinutes"
|
||||
).value
|
||||
);
|
||||
const Seconds = Number(
|
||||
document.getElementById(
|
||||
"AdminMassPlantGrowthSeconds"
|
||||
).value
|
||||
);
|
||||
|
||||
if (
|
||||
!Number.isInteger(Hours) ||
|
||||
!Number.isInteger(Minutes) ||
|
||||
!Number.isInteger(Seconds) ||
|
||||
Hours < 0 ||
|
||||
Minutes < 0 ||
|
||||
Minutes > 59 ||
|
||||
Seconds < 0 ||
|
||||
Seconds > 59
|
||||
) {
|
||||
throw new Error(
|
||||
"Growth time is invalid."
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
Hours * 3600000 +
|
||||
Minutes * 60000 +
|
||||
Seconds * 1000
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function GetAdminMassPlantEffects() {
|
||||
const Raw =
|
||||
document.getElementById(
|
||||
"AdminMassPlantEffects"
|
||||
).value.trim();
|
||||
|
||||
const Effects = JSON.parse(
|
||||
Raw === "" ? "{}" : Raw
|
||||
);
|
||||
|
||||
if (!IsAdminPlainObject(Effects)) {
|
||||
throw new Error(
|
||||
"Effects JSON must be an object."
|
||||
);
|
||||
}
|
||||
|
||||
return Effects;
|
||||
}
|
||||
|
||||
|
||||
function GetAdminMassPlantPatch() {
|
||||
const Patch = {};
|
||||
|
||||
if (AdminMassPlantFieldEnabled("Name")) {
|
||||
Patch.Name =
|
||||
document.getElementById(
|
||||
"AdminMassPlantName"
|
||||
).value.trim();
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassPlantFieldEnabled(
|
||||
"Description"
|
||||
)
|
||||
) {
|
||||
Patch.Description =
|
||||
document.getElementById(
|
||||
"AdminMassPlantDescription"
|
||||
).value.trim();
|
||||
}
|
||||
|
||||
if (AdminMassPlantFieldEnabled("Tags")) {
|
||||
Patch.Tags =
|
||||
ParseAdminMassPlantTags();
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassPlantFieldEnabled(
|
||||
"GrowthTime"
|
||||
)
|
||||
) {
|
||||
Patch.GrowthTime =
|
||||
GetAdminMassPlantGrowthTime();
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassPlantFieldEnabled(
|
||||
"HarvestMultiplier"
|
||||
)
|
||||
) {
|
||||
Patch.HarvestMultiplier =
|
||||
Number(
|
||||
document.getElementById(
|
||||
"AdminMassPlantHarvestMultiplier"
|
||||
).value
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassPlantFieldEnabled(
|
||||
"ShopPlant"
|
||||
)
|
||||
) {
|
||||
Patch.ShopPlant =
|
||||
document.getElementById(
|
||||
"AdminMassPlantShopPlant"
|
||||
).value === "true";
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassPlantFieldEnabled(
|
||||
"BaseCost"
|
||||
)
|
||||
) {
|
||||
Patch.BaseCost =
|
||||
Number(
|
||||
document.getElementById(
|
||||
"AdminMassPlantBaseCost"
|
||||
).value
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
AdminMassPlantFieldEnabled(
|
||||
"Effects"
|
||||
)
|
||||
) {
|
||||
Patch.Effects =
|
||||
GetAdminMassPlantEffects();
|
||||
}
|
||||
|
||||
return Patch;
|
||||
}
|
||||
|
||||
|
||||
function BuildAdminMassPlantPayload(
|
||||
PlantKey,
|
||||
Patch
|
||||
) {
|
||||
const Source =
|
||||
AdminMassPlantCatalogue[
|
||||
PlantKey
|
||||
];
|
||||
|
||||
const Plant = {
|
||||
Id: Number(Source.Id),
|
||||
PlantKey: PlantKey,
|
||||
Name: Source.Name ?? "",
|
||||
Description:
|
||||
Source.Description ?? "",
|
||||
Tags:
|
||||
Array.isArray(Source.Tags)
|
||||
? [...Source.Tags]
|
||||
: [],
|
||||
GrowthTime:
|
||||
Number(Source.GrowthTime ?? 0),
|
||||
HarvestMultiplier:
|
||||
Number(
|
||||
Source.HarvestMultiplier ??
|
||||
1.5
|
||||
),
|
||||
Effects:
|
||||
JSON.parse(
|
||||
JSON.stringify(
|
||||
Source.Effects ?? {}
|
||||
)
|
||||
),
|
||||
ShopPlant:
|
||||
Source.Shop?.ShopPlant === true,
|
||||
BaseCost:
|
||||
Source.Shop?.BaseCost ?? null,
|
||||
IsNew: false
|
||||
};
|
||||
|
||||
Object.assign(
|
||||
Plant,
|
||||
JSON.parse(
|
||||
JSON.stringify(Patch)
|
||||
)
|
||||
);
|
||||
|
||||
return Plant;
|
||||
}
|
||||
|
||||
|
||||
async function SaveAdminMassPlants(
|
||||
Event
|
||||
) {
|
||||
Event.preventDefault();
|
||||
|
||||
if (AdminMassPlantSavePending) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SelectedKeys =
|
||||
GetAdminMassPlantSelectedKeys();
|
||||
|
||||
if (SelectedKeys.length === 0) {
|
||||
SetAdminMassPlantMessage(
|
||||
"Mark at least one plant first."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Patch;
|
||||
|
||||
try {
|
||||
Patch = GetAdminMassPlantPatch();
|
||||
} catch (Error) {
|
||||
SetAdminMassPlantMessage(
|
||||
Error.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(Patch).length === 0) {
|
||||
SetAdminMassPlantMessage(
|
||||
"Enable at least one field to update."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const Payloads = [];
|
||||
|
||||
try {
|
||||
for (const PlantKey of SelectedKeys) {
|
||||
const Plant =
|
||||
BuildAdminMassPlantPayload(
|
||||
PlantKey,
|
||||
Patch
|
||||
);
|
||||
|
||||
ValidateAdminPlant(
|
||||
Plant,
|
||||
AdminMassPlantCatalogue,
|
||||
PlantKey
|
||||
);
|
||||
|
||||
Payloads.push(Plant);
|
||||
}
|
||||
} catch (Error) {
|
||||
SetAdminMassPlantMessage(
|
||||
Error.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
AdminMassPlantSavePending = true;
|
||||
|
||||
const SaveButton =
|
||||
document.getElementById(
|
||||
"AdminMassPlantSaveButton"
|
||||
);
|
||||
|
||||
SaveButton.disabled = true;
|
||||
|
||||
let SavedCount = 0;
|
||||
|
||||
try {
|
||||
for (const Plant of Payloads) {
|
||||
SetAdminMassPlantMessage(
|
||||
"Saving " +
|
||||
(SavedCount + 1) +
|
||||
" of " +
|
||||
Payloads.length +
|
||||
"..."
|
||||
);
|
||||
|
||||
await AdminMassPlantRequest(
|
||||
"Save",
|
||||
{Plant: Plant}
|
||||
);
|
||||
|
||||
SavedCount++;
|
||||
}
|
||||
|
||||
localStorage.removeItem(
|
||||
"SarahtoninGardenContent"
|
||||
);
|
||||
|
||||
await LoadAdminMassPlants();
|
||||
|
||||
SetAdminMassPlantMessage(
|
||||
SavedCount === 1
|
||||
? "1 plant updated."
|
||||
: SavedCount +
|
||||
" plants updated."
|
||||
);
|
||||
} catch (Error) {
|
||||
SetAdminMassPlantMessage(
|
||||
"Saved " +
|
||||
SavedCount +
|
||||
" before stopping: " +
|
||||
(Error.message ??
|
||||
"Couldn't save plants.")
|
||||
);
|
||||
} finally {
|
||||
AdminMassPlantSavePending = false;
|
||||
SaveButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function BindAdminMassPlantEditor() {
|
||||
document.getElementById(
|
||||
"AdminMassPlantSelectAll"
|
||||
).addEventListener(
|
||||
"click",
|
||||
() =>
|
||||
SetAdminMassPlantSelection(
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminMassPlantClear"
|
||||
).addEventListener(
|
||||
"click",
|
||||
() =>
|
||||
SetAdminMassPlantSelection(
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminMassPlantForm"
|
||||
).addEventListener(
|
||||
"submit",
|
||||
SaveAdminMassPlants
|
||||
);
|
||||
|
||||
for (
|
||||
const Apply
|
||||
of document.querySelectorAll(
|
||||
"[data-admin-mass-apply]"
|
||||
)
|
||||
) {
|
||||
Apply.addEventListener(
|
||||
"change",
|
||||
UpdateAdminMassPlantFieldStates
|
||||
);
|
||||
}
|
||||
|
||||
UpdateAdminMassPlantFieldStates();
|
||||
}
|
||||
|
||||
|
||||
async function LoadAdminMassPlants() {
|
||||
const Result =
|
||||
await AdminMassPlantRequest(
|
||||
"List"
|
||||
);
|
||||
|
||||
AdminMassPlantCatalogue =
|
||||
Result.Plants ?? {};
|
||||
|
||||
RenderAdminMassPlantSelection();
|
||||
}
|
||||
|
||||
|
||||
function SetAdminMassPlantMessage(
|
||||
Message
|
||||
) {
|
||||
document.getElementById(
|
||||
"AdminMassPlantMessage"
|
||||
).textContent = Message;
|
||||
}
|
||||
|
||||
|
||||
async function StartAdminMassPlantEditor() {
|
||||
try {
|
||||
const Status =
|
||||
await GetAdminStatus();
|
||||
|
||||
if (
|
||||
Status.Success !== true ||
|
||||
Status.IsAdmin !== true
|
||||
) {
|
||||
SetAdminMassPlantMessage(
|
||||
"Admin access required."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
BindAdminMassPlantEditor();
|
||||
await LoadAdminMassPlants();
|
||||
|
||||
SetAdminMassPlantMessage(
|
||||
"Mark plants and enable the fields you want to change."
|
||||
);
|
||||
} catch (Error) {
|
||||
console.error(
|
||||
"Couldn't start Mass plant editor:",
|
||||
Error
|
||||
);
|
||||
|
||||
SetAdminMassPlantMessage(
|
||||
Error.message ??
|
||||
"Couldn't load the Mass plant editor."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
StartAdminMassPlantEditor
|
||||
);
|
||||
@@ -80,6 +80,31 @@ function BindAdminMutationEditor() {
|
||||
DuplicateAdminMutation
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminMutationImportButton"
|
||||
).addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
document.getElementById(
|
||||
"AdminMutationImportFile"
|
||||
).click();
|
||||
}
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminMutationImportFile"
|
||||
).addEventListener(
|
||||
"change",
|
||||
ImportAdminMutationJson
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminMutationExportButton"
|
||||
).addEventListener(
|
||||
"click",
|
||||
ExportAdminMutationJson
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminMutationResizeButton"
|
||||
).addEventListener(
|
||||
@@ -2339,6 +2364,21 @@ async function SaveAdminMutation(
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ValidateAdminMutation(
|
||||
Mutation,
|
||||
AdminMutationCatalogue,
|
||||
AdminMutationPlantCatalogue,
|
||||
AdminMutationEditingKey
|
||||
);
|
||||
} catch (Error) {
|
||||
SetAdminMutationMessage(
|
||||
Error.message
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
AdminMutationSavePending = true;
|
||||
|
||||
const SaveButton =
|
||||
@@ -2419,6 +2459,458 @@ function RenderAdminMutationJsonPreview() {
|
||||
}
|
||||
|
||||
|
||||
function GetAdminMutationPortableRelations(
|
||||
Relations
|
||||
) {
|
||||
const Result = {
|
||||
PlantsUsed: [],
|
||||
PlantsCreated: []
|
||||
};
|
||||
|
||||
for (
|
||||
const RelationName
|
||||
of [
|
||||
"PlantsUsed",
|
||||
"PlantsCreated"
|
||||
]
|
||||
) {
|
||||
for (
|
||||
const PlantId
|
||||
of Relations?.[
|
||||
RelationName
|
||||
] ?? []
|
||||
) {
|
||||
const PlantKey =
|
||||
GetAdminPlantKeyById(
|
||||
PlantId,
|
||||
AdminMutationPlantCatalogue
|
||||
);
|
||||
|
||||
if (PlantKey !== null) {
|
||||
Result[RelationName].push(
|
||||
PlantKey
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
|
||||
function GetAdminMutationPortableData(
|
||||
Mutation
|
||||
) {
|
||||
return {
|
||||
MutationKey:
|
||||
Mutation.MutationKey,
|
||||
Name: Mutation.Name,
|
||||
Description:
|
||||
Mutation.Description,
|
||||
Hint:
|
||||
Mutation.Hint === ""
|
||||
? null
|
||||
: Mutation.Hint,
|
||||
Priority: Mutation.Priority,
|
||||
Chance: Mutation.Chance,
|
||||
Cooldown: Mutation.Cooldown,
|
||||
Rotation: Mutation.Rotation,
|
||||
AllowImmature:
|
||||
Mutation.AllowImmature === true,
|
||||
Pattern:
|
||||
CloneAdminMutationValue(
|
||||
Mutation.Pattern
|
||||
),
|
||||
Success:
|
||||
CloneAdminMutationValue(
|
||||
Mutation.Success
|
||||
),
|
||||
Failure:
|
||||
CloneAdminMutationValue(
|
||||
Mutation.Failure
|
||||
),
|
||||
Relations:
|
||||
GetAdminMutationPortableRelations(
|
||||
Mutation.Relations
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function ExportAdminMutationJson() {
|
||||
const Form =
|
||||
document.getElementById(
|
||||
"AdminMutationForm"
|
||||
);
|
||||
|
||||
if (!Form.reportValidity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const Mutation =
|
||||
GetAdminMutationFormData();
|
||||
|
||||
ValidateAdminMutation(
|
||||
Mutation,
|
||||
AdminMutationCatalogue,
|
||||
AdminMutationPlantCatalogue,
|
||||
AdminMutationEditingKey
|
||||
);
|
||||
|
||||
DownloadAdminJson(
|
||||
"Mutation-" +
|
||||
Mutation.MutationKey +
|
||||
".json",
|
||||
{
|
||||
Type: "Mutation",
|
||||
Version: 1,
|
||||
Mutation:
|
||||
GetAdminMutationPortableData(
|
||||
Mutation
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
SetAdminMutationMessage(
|
||||
"Mutation JSON exported. Numeric IDs are not included."
|
||||
);
|
||||
} catch (Error) {
|
||||
SetAdminMutationMessage(
|
||||
Error.message ??
|
||||
"Couldn't export mutation JSON."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function ConvertAdminMutationPortableRelations(
|
||||
Relations
|
||||
) {
|
||||
if (
|
||||
Relations === undefined ||
|
||||
Relations === null
|
||||
) {
|
||||
return {
|
||||
PlantsUsed: [],
|
||||
PlantsCreated: []
|
||||
};
|
||||
}
|
||||
|
||||
if (!IsAdminPlainObject(Relations)) {
|
||||
throw new Error(
|
||||
"Relations must be an object."
|
||||
);
|
||||
}
|
||||
|
||||
const Result = {
|
||||
PlantsUsed: [],
|
||||
PlantsCreated: []
|
||||
};
|
||||
|
||||
for (
|
||||
const RelationName
|
||||
of [
|
||||
"PlantsUsed",
|
||||
"PlantsCreated"
|
||||
]
|
||||
) {
|
||||
const Values =
|
||||
Relations[RelationName] ?? [];
|
||||
|
||||
if (!Array.isArray(Values)) {
|
||||
throw new Error(
|
||||
RelationName +
|
||||
" must be an array of Plant Keys."
|
||||
);
|
||||
}
|
||||
|
||||
for (const Value of Values) {
|
||||
if (typeof Value !== "string") {
|
||||
throw new Error(
|
||||
RelationName +
|
||||
" must use Plant Keys, not numeric IDs."
|
||||
);
|
||||
}
|
||||
|
||||
const Plant =
|
||||
AdminMutationPlantCatalogue[
|
||||
Value
|
||||
];
|
||||
|
||||
if (Plant === undefined) {
|
||||
throw new Error(
|
||||
RelationName +
|
||||
" references unknown Plant Key: " +
|
||||
Value
|
||||
);
|
||||
}
|
||||
|
||||
Result[RelationName].push(
|
||||
Number(Plant.Id)
|
||||
);
|
||||
}
|
||||
|
||||
Result[RelationName] = [
|
||||
...new Set(
|
||||
Result[RelationName]
|
||||
)
|
||||
].sort(
|
||||
(A, B) => A - B
|
||||
);
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
|
||||
async function ImportAdminMutationJson(
|
||||
Event
|
||||
) {
|
||||
const Input = Event.target;
|
||||
const File = Input.files?.[0];
|
||||
|
||||
Input.value = "";
|
||||
|
||||
if (File === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const DocumentData =
|
||||
await ReadAdminJsonFile(
|
||||
File
|
||||
);
|
||||
|
||||
if (
|
||||
IsAdminPlainObject(
|
||||
DocumentData
|
||||
) &&
|
||||
DocumentData.Type !==
|
||||
undefined &&
|
||||
DocumentData.Type !== "Mutation"
|
||||
) {
|
||||
throw new Error(
|
||||
"That JSON file is not a mutation export."
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
IsAdminPlainObject(
|
||||
DocumentData
|
||||
) &&
|
||||
DocumentData.Version !==
|
||||
undefined &&
|
||||
Number(DocumentData.Version) !== 1
|
||||
) {
|
||||
throw new Error(
|
||||
"Unsupported mutation JSON version."
|
||||
);
|
||||
}
|
||||
|
||||
const Imported =
|
||||
IsAdminPlainObject(
|
||||
DocumentData?.Mutation
|
||||
)
|
||||
? DocumentData.Mutation
|
||||
: DocumentData;
|
||||
|
||||
if (!IsAdminPlainObject(Imported)) {
|
||||
throw new Error(
|
||||
"Mutation JSON must contain a mutation object."
|
||||
);
|
||||
}
|
||||
|
||||
const MutationKey =
|
||||
String(
|
||||
Imported.MutationKey ?? ""
|
||||
).trim();
|
||||
|
||||
const Existing =
|
||||
AdminMutationCatalogue[
|
||||
MutationKey
|
||||
];
|
||||
|
||||
const Mutation = {
|
||||
Id:
|
||||
Existing?.Id ??
|
||||
GetNextAdminMutationId(),
|
||||
MutationKey: MutationKey,
|
||||
Name:
|
||||
String(
|
||||
Imported.Name ?? ""
|
||||
).trim(),
|
||||
Description:
|
||||
typeof Imported.Description ===
|
||||
"string"
|
||||
? Imported.Description.trim()
|
||||
: "",
|
||||
Hint:
|
||||
typeof Imported.Hint ===
|
||||
"string"
|
||||
? Imported.Hint.trim()
|
||||
: "",
|
||||
Priority:
|
||||
Number(
|
||||
Imported.Priority ?? 0
|
||||
),
|
||||
Chance:
|
||||
Number(
|
||||
Imported.Chance
|
||||
),
|
||||
Cooldown:
|
||||
Number(
|
||||
Imported.Cooldown
|
||||
),
|
||||
Rotation:
|
||||
Imported.Rotation ??
|
||||
"None",
|
||||
AllowImmature:
|
||||
Imported.AllowImmature ===
|
||||
true,
|
||||
Pattern:
|
||||
CloneAdminMutationValue(
|
||||
Imported.Pattern
|
||||
),
|
||||
Success:
|
||||
CloneAdminMutationValue(
|
||||
Imported.Success
|
||||
),
|
||||
Failure:
|
||||
CloneAdminMutationValue(
|
||||
Imported.Failure ??
|
||||
"Keep"
|
||||
),
|
||||
Relations:
|
||||
ConvertAdminMutationPortableRelations(
|
||||
Imported.Relations
|
||||
),
|
||||
IsNew:
|
||||
Existing === undefined
|
||||
};
|
||||
|
||||
ValidateAdminMutation(
|
||||
Mutation,
|
||||
AdminMutationCatalogue,
|
||||
AdminMutationPlantCatalogue,
|
||||
Existing === undefined
|
||||
? null
|
||||
: MutationKey
|
||||
);
|
||||
|
||||
if (Existing === undefined) {
|
||||
StartNewAdminMutation();
|
||||
AdminMutationEditingKey = null;
|
||||
} else {
|
||||
LoadAdminMutationIntoForm(
|
||||
MutationKey
|
||||
);
|
||||
}
|
||||
|
||||
SetAdminMutationField(
|
||||
"AdminMutationId",
|
||||
Mutation.Id
|
||||
);
|
||||
SetAdminMutationField(
|
||||
"AdminMutationKey",
|
||||
Mutation.MutationKey
|
||||
);
|
||||
SetAdminMutationField(
|
||||
"AdminMutationName",
|
||||
Mutation.Name
|
||||
);
|
||||
SetAdminMutationField(
|
||||
"AdminMutationDescription",
|
||||
Mutation.Description
|
||||
);
|
||||
SetAdminMutationField(
|
||||
"AdminMutationHint",
|
||||
Mutation.Hint
|
||||
);
|
||||
SetAdminMutationField(
|
||||
"AdminMutationPriority",
|
||||
Mutation.Priority
|
||||
);
|
||||
SetAdminMutationField(
|
||||
"AdminMutationChance",
|
||||
Mutation.Chance * 100
|
||||
);
|
||||
SetAdminMutationDurationFields(
|
||||
Mutation.Cooldown
|
||||
);
|
||||
SetAdminMutationField(
|
||||
"AdminMutationRotation",
|
||||
Mutation.Rotation
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminMutationAllowImmature"
|
||||
).checked =
|
||||
Mutation.AllowImmature;
|
||||
|
||||
AdminMutationPattern =
|
||||
CloneAdminMutationMatrix(
|
||||
Mutation.Pattern,
|
||||
"Any"
|
||||
);
|
||||
AdminMutationResult =
|
||||
NormalizeAdminMutationMatrix(
|
||||
Mutation.Success,
|
||||
AdminMutationPattern[0]
|
||||
.length,
|
||||
AdminMutationPattern.length,
|
||||
"Keep"
|
||||
);
|
||||
|
||||
SetAdminMutationField(
|
||||
"AdminMutationWidth",
|
||||
AdminMutationPattern[0]
|
||||
.length
|
||||
);
|
||||
SetAdminMutationField(
|
||||
"AdminMutationHeight",
|
||||
AdminMutationPattern.length
|
||||
);
|
||||
|
||||
LoadAdminMutationFailure(
|
||||
Mutation.Failure
|
||||
);
|
||||
SetAdminMutationRelations(
|
||||
Mutation.Relations
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminMutationId"
|
||||
).readOnly =
|
||||
Existing !== undefined;
|
||||
document.getElementById(
|
||||
"AdminMutationKey"
|
||||
).readOnly =
|
||||
Existing !== undefined;
|
||||
|
||||
AdminMutationSelectedCell = null;
|
||||
HideAdminMutationCellEditor();
|
||||
RenderAdminMutationGrids();
|
||||
UpdateAdminMutationFailureVisibility();
|
||||
UpdateAdminMutationCooldownHint();
|
||||
RenderAdminMutationJsonPreview();
|
||||
|
||||
SetAdminMutationMessage(
|
||||
Existing === undefined
|
||||
? "Mutation JSON imported as a new mutation. Review it, then save when ready."
|
||||
: "Mutation JSON imported over " +
|
||||
MutationKey +
|
||||
". Review it, then save when ready."
|
||||
);
|
||||
} catch (Error) {
|
||||
SetAdminMutationMessage(
|
||||
Error.message ??
|
||||
"Couldn't import mutation JSON."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function SetAdminMutationDurationFields(
|
||||
Milliseconds
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
let AdminOverviewPlants = {};
|
||||
let AdminOverviewMutations = {};
|
||||
|
||||
|
||||
async function AdminOverviewRequest(
|
||||
Endpoint,
|
||||
Action = "List"
|
||||
) {
|
||||
const SaveKey =
|
||||
GetStoredAdminSaveKey();
|
||||
|
||||
if (SaveKey === null) {
|
||||
throw new Error(
|
||||
"A valid Account Key is required."
|
||||
);
|
||||
}
|
||||
|
||||
const Response = await fetch(
|
||||
AdminApiUrl + "/" + Endpoint,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type":
|
||||
"application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
SaveKey: SaveKey,
|
||||
Action: Action
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const Result =
|
||||
await Response.json();
|
||||
|
||||
if (
|
||||
!Response.ok ||
|
||||
Result.Success !== true
|
||||
) {
|
||||
throw new Error(
|
||||
Result.Error ??
|
||||
"Admin overview request failed."
|
||||
);
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
|
||||
function GetAdminOverviewPlantKeyById(
|
||||
PlantId
|
||||
) {
|
||||
return Object.entries(
|
||||
AdminOverviewPlants
|
||||
).find(
|
||||
([, Plant]) =>
|
||||
Number(Plant.Id) ===
|
||||
Number(PlantId)
|
||||
)?.[0] ?? null;
|
||||
}
|
||||
|
||||
|
||||
function GetAdminOverviewMissingSprites(
|
||||
PlantKey,
|
||||
Plant
|
||||
) {
|
||||
const StaticImages =
|
||||
Array.isArray(
|
||||
PlantImages?.[PlantKey]
|
||||
)
|
||||
? PlantImages[PlantKey]
|
||||
: [];
|
||||
|
||||
const ApiStages =
|
||||
new Set(
|
||||
(
|
||||
Array.isArray(
|
||||
Plant.ImageStages
|
||||
)
|
||||
? Plant.ImageStages
|
||||
: []
|
||||
)
|
||||
.map(
|
||||
Stage =>
|
||||
Number(
|
||||
Stage?.Stage ??
|
||||
Stage
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
Stage =>
|
||||
Number.isInteger(Stage) &&
|
||||
Stage > 0
|
||||
)
|
||||
);
|
||||
|
||||
const HighestApiStage =
|
||||
Math.max(
|
||||
0,
|
||||
...ApiStages
|
||||
);
|
||||
|
||||
const HighestStage =
|
||||
Math.max(
|
||||
3,
|
||||
StaticImages.length,
|
||||
HighestApiStage
|
||||
);
|
||||
|
||||
const Missing = [];
|
||||
|
||||
for (
|
||||
let Stage = 1;
|
||||
Stage <= HighestStage;
|
||||
Stage++
|
||||
) {
|
||||
if (
|
||||
ApiStages.has(Stage) ||
|
||||
typeof StaticImages[
|
||||
Stage - 1
|
||||
] === "string"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Missing.push(
|
||||
Stage + ".png"
|
||||
);
|
||||
}
|
||||
|
||||
return Missing;
|
||||
}
|
||||
|
||||
|
||||
function CollectAdminOverviewCreatedPlants() {
|
||||
const Created = new Set();
|
||||
|
||||
function AddResultValue(Value) {
|
||||
if (typeof Value === "string") {
|
||||
if (
|
||||
Value !== "Keep" &&
|
||||
Value !== "Empty" &&
|
||||
!Value.startsWith("$") &&
|
||||
AdminOverviewPlants[
|
||||
Value
|
||||
] !== undefined
|
||||
) {
|
||||
Created.add(Value);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
Value !== null &&
|
||||
typeof Value === "object" &&
|
||||
!Array.isArray(Value) &&
|
||||
typeof Value.Plant === "string"
|
||||
) {
|
||||
AddResultValue(
|
||||
Value.Plant
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (
|
||||
const Mutation
|
||||
of Object.values(
|
||||
AdminOverviewMutations
|
||||
)
|
||||
) {
|
||||
if (Mutation.Archived === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (
|
||||
const PlantId
|
||||
of Mutation.Relations
|
||||
?.PlantsCreated ?? []
|
||||
) {
|
||||
const PlantKey =
|
||||
GetAdminOverviewPlantKeyById(
|
||||
PlantId
|
||||
);
|
||||
|
||||
if (PlantKey !== null) {
|
||||
Created.add(PlantKey);
|
||||
}
|
||||
}
|
||||
|
||||
for (
|
||||
const Row
|
||||
of Mutation.Success ?? []
|
||||
) {
|
||||
if (!Array.isArray(Row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const Cell of Row) {
|
||||
AddResultValue(Cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Created;
|
||||
}
|
||||
|
||||
|
||||
function CreateAdminOverviewIssueList(
|
||||
Items,
|
||||
NestedValues = null
|
||||
) {
|
||||
const List =
|
||||
document.createElement("ul");
|
||||
|
||||
List.className =
|
||||
"AdminOverviewList";
|
||||
|
||||
for (const Item of Items) {
|
||||
const ListItem =
|
||||
document.createElement("li");
|
||||
|
||||
const Label =
|
||||
document.createElement("span");
|
||||
|
||||
Label.textContent = Item.Label;
|
||||
ListItem.appendChild(Label);
|
||||
|
||||
const Nested =
|
||||
NestedValues?.(Item) ?? [];
|
||||
|
||||
if (Nested.length > 0) {
|
||||
const NestedList =
|
||||
document.createElement("ul");
|
||||
|
||||
for (const Value of Nested) {
|
||||
const NestedItem =
|
||||
document.createElement("li");
|
||||
|
||||
NestedItem.textContent =
|
||||
Value;
|
||||
NestedList.appendChild(
|
||||
NestedItem
|
||||
);
|
||||
}
|
||||
|
||||
ListItem.appendChild(
|
||||
NestedList
|
||||
);
|
||||
}
|
||||
|
||||
List.appendChild(ListItem);
|
||||
}
|
||||
|
||||
return List;
|
||||
}
|
||||
|
||||
|
||||
function RenderAdminOverviewSection(
|
||||
ContainerId,
|
||||
Title,
|
||||
Items,
|
||||
NestedValues = null
|
||||
) {
|
||||
const Container =
|
||||
document.getElementById(
|
||||
ContainerId
|
||||
);
|
||||
|
||||
Container.replaceChildren();
|
||||
|
||||
const Heading =
|
||||
document.createElement("h3");
|
||||
|
||||
Heading.textContent = Title;
|
||||
Container.appendChild(Heading);
|
||||
|
||||
if (Items.length === 0) {
|
||||
const Empty =
|
||||
document.createElement("p");
|
||||
|
||||
Empty.className =
|
||||
"AdminOverviewEmpty";
|
||||
Empty.textContent = "None";
|
||||
Container.appendChild(Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
Container.appendChild(
|
||||
CreateAdminOverviewIssueList(
|
||||
Items,
|
||||
NestedValues
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function RenderAdminOverview() {
|
||||
const CreatedPlants =
|
||||
CollectAdminOverviewCreatedPlants();
|
||||
|
||||
const PlantEntries =
|
||||
Object.entries(
|
||||
AdminOverviewPlants
|
||||
)
|
||||
.filter(
|
||||
([, Plant]) =>
|
||||
Plant.Archived !== true
|
||||
)
|
||||
.sort(
|
||||
([KeyA, PlantA], [KeyB, PlantB]) =>
|
||||
Number(PlantA.Id) -
|
||||
Number(PlantB.Id) ||
|
||||
KeyA.localeCompare(KeyB)
|
||||
);
|
||||
|
||||
const MissingSprites = [];
|
||||
const NoWaysToObtain = [];
|
||||
|
||||
for (
|
||||
const [PlantKey, Plant]
|
||||
of PlantEntries
|
||||
) {
|
||||
const Missing =
|
||||
GetAdminOverviewMissingSprites(
|
||||
PlantKey,
|
||||
Plant
|
||||
);
|
||||
|
||||
if (Missing.length > 0) {
|
||||
MissingSprites.push({
|
||||
Label:
|
||||
Plant.Name ?? PlantKey,
|
||||
Missing: Missing
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
Plant.Shop?.ShopPlant !== true &&
|
||||
!CreatedPlants.has(PlantKey)
|
||||
) {
|
||||
NoWaysToObtain.push({
|
||||
Label:
|
||||
Plant.Name ?? PlantKey
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const MutationEntries =
|
||||
Object.entries(
|
||||
AdminOverviewMutations
|
||||
)
|
||||
.filter(
|
||||
([, Mutation]) =>
|
||||
Mutation.Archived !== true
|
||||
)
|
||||
.sort(
|
||||
([KeyA, MutationA], [KeyB, MutationB]) =>
|
||||
Number(MutationA.Id) -
|
||||
Number(MutationB.Id) ||
|
||||
KeyA.localeCompare(KeyB)
|
||||
);
|
||||
|
||||
const NoDescription = [];
|
||||
const NoHint = [];
|
||||
|
||||
for (
|
||||
const [MutationKey, Mutation]
|
||||
of MutationEntries
|
||||
) {
|
||||
const Label =
|
||||
Mutation.Name?.trim() ||
|
||||
MutationKey;
|
||||
|
||||
if (
|
||||
typeof Mutation.Description !==
|
||||
"string" ||
|
||||
Mutation.Description.trim() === ""
|
||||
) {
|
||||
NoDescription.push({
|
||||
Label: Label
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
typeof Mutation.Hint !==
|
||||
"string" ||
|
||||
Mutation.Hint.trim() === ""
|
||||
) {
|
||||
NoHint.push({
|
||||
Label: Label
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
RenderAdminOverviewSection(
|
||||
"AdminOverviewMissingSprites",
|
||||
"Missing sprites",
|
||||
MissingSprites,
|
||||
Item => Item.Missing
|
||||
);
|
||||
|
||||
RenderAdminOverviewSection(
|
||||
"AdminOverviewNoObtain",
|
||||
"No ways to obtain",
|
||||
NoWaysToObtain
|
||||
);
|
||||
|
||||
RenderAdminOverviewSection(
|
||||
"AdminOverviewNoDescription",
|
||||
"No description",
|
||||
NoDescription
|
||||
);
|
||||
|
||||
RenderAdminOverviewSection(
|
||||
"AdminOverviewNoHint",
|
||||
"No hint",
|
||||
NoHint
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
async function StartAdminOverview() {
|
||||
const Overview =
|
||||
document.getElementById(
|
||||
"AdminOverview"
|
||||
);
|
||||
|
||||
if (Overview === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const Status =
|
||||
await GetAdminStatus();
|
||||
|
||||
if (
|
||||
Status.Success !== true ||
|
||||
Status.IsAdmin !== true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [
|
||||
PlantResult,
|
||||
MutationResult
|
||||
] = await Promise.all([
|
||||
AdminOverviewRequest(
|
||||
"AdminPlants.php"
|
||||
),
|
||||
AdminOverviewRequest(
|
||||
"AdminMutations.php"
|
||||
)
|
||||
]);
|
||||
|
||||
AdminOverviewPlants =
|
||||
PlantResult.Plants ?? {};
|
||||
AdminOverviewMutations =
|
||||
MutationResult.Mutations ?? {};
|
||||
|
||||
RenderAdminOverview();
|
||||
Overview.hidden = false;
|
||||
} catch (Error) {
|
||||
console.error(
|
||||
"Couldn't load admin overview:",
|
||||
Error
|
||||
);
|
||||
|
||||
const Message =
|
||||
document.getElementById(
|
||||
"AdminOverviewMessage"
|
||||
);
|
||||
|
||||
Message.textContent =
|
||||
Error.message ??
|
||||
"Couldn't load admin overview.";
|
||||
Overview.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
StartAdminOverview
|
||||
);
|
||||
@@ -71,6 +71,31 @@ function BindAdminPlantEditor() {
|
||||
DuplicateAdminPlant
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminPlantImportButton"
|
||||
).addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
document.getElementById(
|
||||
"AdminPlantImportFile"
|
||||
).click();
|
||||
}
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminPlantImportFile"
|
||||
).addEventListener(
|
||||
"change",
|
||||
ImportAdminPlantJson
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminPlantExportButton"
|
||||
).addEventListener(
|
||||
"click",
|
||||
ExportAdminPlantJson
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminPlantForm"
|
||||
).addEventListener(
|
||||
@@ -930,6 +955,20 @@ async function SaveAdminPlant(
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ValidateAdminPlant(
|
||||
Plant,
|
||||
AdminPlantCatalogue,
|
||||
AdminPlantEditingKey
|
||||
);
|
||||
} catch (Error) {
|
||||
SetAdminPlantMessage(
|
||||
Error.message
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
AdminPlantSavePending = true;
|
||||
|
||||
const SaveButton =
|
||||
@@ -1833,6 +1872,297 @@ async function DeleteAdminPlantImage(
|
||||
}
|
||||
|
||||
|
||||
function GetAdminPlantPortableData(
|
||||
Plant
|
||||
) {
|
||||
return {
|
||||
PlantKey: Plant.PlantKey,
|
||||
Name: Plant.Name,
|
||||
Description: Plant.Description,
|
||||
Tags: [...Plant.Tags],
|
||||
GrowthTime: Plant.GrowthTime,
|
||||
HarvestMultiplier:
|
||||
Plant.HarvestMultiplier,
|
||||
Effects: JSON.parse(
|
||||
JSON.stringify(
|
||||
Plant.Effects ?? {}
|
||||
)
|
||||
),
|
||||
Shop: {
|
||||
ShopPlant:
|
||||
Plant.ShopPlant === true,
|
||||
BaseCost:
|
||||
Plant.ShopPlant === true
|
||||
? Plant.BaseCost
|
||||
: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function ExportAdminPlantJson() {
|
||||
const Form =
|
||||
document.getElementById(
|
||||
"AdminPlantForm"
|
||||
);
|
||||
|
||||
if (!Form.reportValidity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const Plant =
|
||||
GetAdminPlantFormData();
|
||||
|
||||
ValidateAdminPlant(
|
||||
Plant,
|
||||
AdminPlantCatalogue,
|
||||
AdminPlantEditingKey
|
||||
);
|
||||
|
||||
DownloadAdminJson(
|
||||
"Plant-" +
|
||||
Plant.PlantKey +
|
||||
".json",
|
||||
{
|
||||
Type: "Plant",
|
||||
Version: 1,
|
||||
Plant:
|
||||
GetAdminPlantPortableData(
|
||||
Plant
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
SetAdminPlantMessage(
|
||||
"Plant JSON exported. Sprites and numeric ID are not included."
|
||||
);
|
||||
} catch (Error) {
|
||||
SetAdminPlantMessage(
|
||||
Error.message ??
|
||||
"Couldn't export plant JSON."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function ImportAdminPlantJson(
|
||||
Event
|
||||
) {
|
||||
const Input = Event.target;
|
||||
const File = Input.files?.[0];
|
||||
|
||||
Input.value = "";
|
||||
|
||||
if (File === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const DocumentData =
|
||||
await ReadAdminJsonFile(
|
||||
File
|
||||
);
|
||||
|
||||
if (
|
||||
IsAdminPlainObject(
|
||||
DocumentData
|
||||
) &&
|
||||
DocumentData.Type !==
|
||||
undefined &&
|
||||
DocumentData.Type !== "Plant"
|
||||
) {
|
||||
throw new Error(
|
||||
"That JSON file is not a plant export."
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
IsAdminPlainObject(
|
||||
DocumentData
|
||||
) &&
|
||||
DocumentData.Version !==
|
||||
undefined &&
|
||||
Number(DocumentData.Version) !== 1
|
||||
) {
|
||||
throw new Error(
|
||||
"Unsupported plant JSON version."
|
||||
);
|
||||
}
|
||||
|
||||
const Imported =
|
||||
IsAdminPlainObject(
|
||||
DocumentData?.Plant
|
||||
)
|
||||
? DocumentData.Plant
|
||||
: DocumentData;
|
||||
|
||||
if (!IsAdminPlainObject(Imported)) {
|
||||
throw new Error(
|
||||
"Plant JSON must contain a plant object."
|
||||
);
|
||||
}
|
||||
|
||||
const PlantKey =
|
||||
String(
|
||||
Imported.PlantKey ?? ""
|
||||
).trim();
|
||||
|
||||
const Existing =
|
||||
AdminPlantCatalogue[
|
||||
PlantKey
|
||||
];
|
||||
|
||||
const Shop =
|
||||
IsAdminPlainObject(
|
||||
Imported.Shop
|
||||
)
|
||||
? Imported.Shop
|
||||
: {};
|
||||
|
||||
const Plant = {
|
||||
Id:
|
||||
Existing?.Id ??
|
||||
GetNextAdminPlantId(),
|
||||
PlantKey: PlantKey,
|
||||
Name:
|
||||
String(
|
||||
Imported.Name ?? ""
|
||||
).trim(),
|
||||
Description:
|
||||
typeof Imported.Description ===
|
||||
"string"
|
||||
? Imported.Description.trim()
|
||||
: "",
|
||||
Tags:
|
||||
Array.isArray(Imported.Tags)
|
||||
? Imported.Tags.map(
|
||||
Tag =>
|
||||
String(Tag).trim()
|
||||
)
|
||||
: [],
|
||||
GrowthTime:
|
||||
Number(
|
||||
Imported.GrowthTime
|
||||
),
|
||||
HarvestMultiplier:
|
||||
Number(
|
||||
Imported.HarvestMultiplier ??
|
||||
1.5
|
||||
),
|
||||
Effects:
|
||||
IsAdminPlainObject(
|
||||
Imported.Effects
|
||||
)
|
||||
? JSON.parse(
|
||||
JSON.stringify(
|
||||
Imported.Effects
|
||||
)
|
||||
)
|
||||
: {},
|
||||
ShopPlant:
|
||||
Shop.ShopPlant === true ||
|
||||
Imported.ShopPlant === true,
|
||||
BaseCost:
|
||||
Shop.BaseCost ??
|
||||
Imported.BaseCost ??
|
||||
null,
|
||||
IsNew:
|
||||
Existing === undefined
|
||||
};
|
||||
|
||||
ValidateAdminPlant(
|
||||
Plant,
|
||||
AdminPlantCatalogue,
|
||||
Existing === undefined
|
||||
? null
|
||||
: PlantKey
|
||||
);
|
||||
|
||||
if (Existing === undefined) {
|
||||
StartNewAdminPlant();
|
||||
AdminPlantEditingKey = null;
|
||||
} else {
|
||||
LoadAdminPlantIntoForm(
|
||||
PlantKey
|
||||
);
|
||||
}
|
||||
|
||||
SetAdminPlantField(
|
||||
"AdminPlantId",
|
||||
Plant.Id
|
||||
);
|
||||
SetAdminPlantField(
|
||||
"AdminPlantKey",
|
||||
Plant.PlantKey
|
||||
);
|
||||
SetAdminPlantField(
|
||||
"AdminPlantName",
|
||||
Plant.Name
|
||||
);
|
||||
SetAdminPlantField(
|
||||
"AdminPlantDescription",
|
||||
Plant.Description
|
||||
);
|
||||
SetAdminPlantField(
|
||||
"AdminPlantTags",
|
||||
Plant.Tags.join(", ")
|
||||
);
|
||||
SetAdminPlantDurationFields(
|
||||
Plant.GrowthTime
|
||||
);
|
||||
SetAdminPlantField(
|
||||
"AdminPlantHarvestMultiplier",
|
||||
Plant.HarvestMultiplier
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminPlantShopPlant"
|
||||
).checked = Plant.ShopPlant;
|
||||
|
||||
SetAdminPlantField(
|
||||
"AdminPlantBaseCost",
|
||||
Plant.BaseCost ?? ""
|
||||
);
|
||||
SetAdminPlantField(
|
||||
"AdminPlantEffects",
|
||||
JSON.stringify(
|
||||
Plant.Effects,
|
||||
null,
|
||||
4
|
||||
)
|
||||
);
|
||||
|
||||
document.getElementById(
|
||||
"AdminPlantId"
|
||||
).readOnly =
|
||||
Existing !== undefined;
|
||||
|
||||
document.getElementById(
|
||||
"AdminPlantKey"
|
||||
).readOnly =
|
||||
Existing !== undefined;
|
||||
|
||||
UpdateAdminPlantShopFields();
|
||||
RenderAdminPlantPreview();
|
||||
RenderAdminPlantImageManager();
|
||||
|
||||
SetAdminPlantMessage(
|
||||
Existing === undefined
|
||||
? "Plant JSON imported as a new plant. Review it, then save when ready."
|
||||
: "Plant JSON imported over " +
|
||||
PlantKey +
|
||||
". Review it, then save when ready."
|
||||
);
|
||||
} catch (Error) {
|
||||
SetAdminPlantMessage(
|
||||
Error.message ??
|
||||
"Couldn't import plant JSON."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function FormatAdminDuration(
|
||||
Milliseconds
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,820 @@
|
||||
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 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."
|
||||
);
|
||||
}
|
||||
|
||||
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 ValidateAdminMutationPatternCell(
|
||||
Cell,
|
||||
PlantCatalogue,
|
||||
Captures
|
||||
) {
|
||||
if (
|
||||
Cell === null ||
|
||||
Cell === "Any" ||
|
||||
Cell === "Empty"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof Cell === "string") {
|
||||
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
|
||||
) {
|
||||
if (
|
||||
Cell === null ||
|
||||
Cell === "Keep" ||
|
||||
Cell === "Empty"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let PlantValue = null;
|
||||
|
||||
if (typeof Cell === "string") {
|
||||
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."
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ValidateAdminMutationMatrix(
|
||||
Mutation.Success,
|
||||
"Success result",
|
||||
PatternSize.Width,
|
||||
PatternSize.Height
|
||||
);
|
||||
|
||||
for (const Row of Mutation.Success) {
|
||||
for (const Cell of Row) {
|
||||
ValidateAdminMutationResultCell(
|
||||
Cell,
|
||||
PlantCatalogue,
|
||||
Captures
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user