mirror of
https://git.srhtnin.garden/sarah/srhtnin.github.io.git
synced 2026-10-13 18:38:11 +02:00
2484 lines
51 KiB
JavaScript
2484 lines
51 KiB
JavaScript
let AdminPlantCatalogue = {};
|
||
let AdminPlantMutationCatalogue = {};
|
||
let AdminPlantEditingKey = null;
|
||
let AdminPlantSavePending = false;
|
||
let AdminPlantImagePending = false;
|
||
|
||
|
||
async function StartAdminPlantEditor() {
|
||
try {
|
||
const Status =
|
||
await GetAdminStatus();
|
||
|
||
if (
|
||
Status.Success !== true ||
|
||
Status.IsAdmin !== true
|
||
) {
|
||
SetAdminPlantMessage(
|
||
"Admin access required."
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
BindAdminPlantEditor();
|
||
await LoadAdminPlants();
|
||
} catch (Error) {
|
||
console.error(
|
||
"Couldn't start Plant editor:",
|
||
Error
|
||
);
|
||
|
||
SetAdminPlantMessage(
|
||
Error.message ??
|
||
"Couldn't load the Plant editor."
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
function BindAdminPlantEditor() {
|
||
document.getElementById(
|
||
"AdminPlantSelect"
|
||
).addEventListener(
|
||
"change",
|
||
Event => {
|
||
const PlantKey =
|
||
Event.target.value;
|
||
|
||
if (PlantKey === "") {
|
||
StartNewAdminPlant();
|
||
return;
|
||
}
|
||
|
||
LoadAdminPlantIntoForm(
|
||
PlantKey
|
||
);
|
||
}
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantNewButton"
|
||
).addEventListener(
|
||
"click",
|
||
StartNewAdminPlant
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantDuplicateButton"
|
||
).addEventListener(
|
||
"click",
|
||
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(
|
||
"submit",
|
||
SaveAdminPlant
|
||
);
|
||
|
||
for (
|
||
const ElementId
|
||
of [
|
||
"AdminPlantId",
|
||
"AdminPlantKey",
|
||
"AdminPlantName",
|
||
"AdminPlantHarvestMultiplier",
|
||
"AdminPlantShopPlant",
|
||
"AdminPlantBaseCost"
|
||
]
|
||
) {
|
||
document.getElementById(
|
||
ElementId
|
||
).addEventListener(
|
||
"input",
|
||
RenderAdminPlantPreview
|
||
);
|
||
|
||
document.getElementById(
|
||
ElementId
|
||
).addEventListener(
|
||
"change",
|
||
RenderAdminPlantPreview
|
||
);
|
||
}
|
||
|
||
for (
|
||
const ElementId
|
||
of [
|
||
"AdminPlantGrowthHours",
|
||
"AdminPlantGrowthMinutes",
|
||
"AdminPlantGrowthSeconds"
|
||
]
|
||
) {
|
||
document.getElementById(
|
||
ElementId
|
||
).addEventListener(
|
||
"input",
|
||
() => {
|
||
SyncAdminPlantGrowthTime();
|
||
RenderAdminPlantPreview();
|
||
}
|
||
);
|
||
|
||
document.getElementById(
|
||
ElementId
|
||
).addEventListener(
|
||
"change",
|
||
() => {
|
||
SyncAdminPlantGrowthTime();
|
||
RenderAdminPlantPreview();
|
||
}
|
||
);
|
||
}
|
||
|
||
document.getElementById(
|
||
"AdminPlantShopPlant"
|
||
).addEventListener(
|
||
"change",
|
||
() => {
|
||
UpdateAdminPlantShopFields();
|
||
RenderAdminPlantPreview();
|
||
}
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantDirectionalSprites"
|
||
).addEventListener(
|
||
"change",
|
||
() => {
|
||
RenderAdminPlantImageManager();
|
||
RenderAdminPlantPreview();
|
||
}
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantImageUploadButton"
|
||
).addEventListener(
|
||
"click",
|
||
UploadAdminPlantImages
|
||
);
|
||
}
|
||
|
||
|
||
async function AdminPlantRequest(
|
||
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
|
||
})
|
||
}
|
||
);
|
||
|
||
let Result;
|
||
|
||
try {
|
||
Result = await Response.json();
|
||
} catch (Error) {
|
||
throw new Error(
|
||
"Plant admin API returned invalid JSON."
|
||
);
|
||
}
|
||
|
||
if (
|
||
!Response.ok ||
|
||
Result.Success !== true
|
||
) {
|
||
throw new Error(
|
||
Result.Error ??
|
||
"Plant admin request failed."
|
||
);
|
||
}
|
||
|
||
return Result;
|
||
}
|
||
|
||
|
||
async function AdminPlantMutationRequest() {
|
||
const SaveKey =
|
||
localStorage.getItem(
|
||
SaveKeyName
|
||
);
|
||
|
||
const Response = await fetch(
|
||
ApiUrl +
|
||
"/AdminMutations.php",
|
||
{
|
||
method: "POST",
|
||
|
||
headers: {
|
||
"Content-Type":
|
||
"application/json"
|
||
},
|
||
|
||
body: JSON.stringify({
|
||
SaveKey: SaveKey,
|
||
Action: "List"
|
||
})
|
||
}
|
||
);
|
||
|
||
const Result =
|
||
await Response.json();
|
||
|
||
if (
|
||
!Response.ok ||
|
||
Result.Success !== true
|
||
) {
|
||
throw new Error(
|
||
Result.Error ??
|
||
"Couldn't load mutations for the Plant editor."
|
||
);
|
||
}
|
||
|
||
return Result;
|
||
}
|
||
|
||
|
||
async function LoadAdminPlants(
|
||
PreferredPlantKey = null
|
||
) {
|
||
SetAdminPlantMessage(
|
||
"Loading plants..."
|
||
);
|
||
|
||
const [
|
||
PlantResult,
|
||
MutationResult
|
||
] = await Promise.all([
|
||
AdminPlantRequest(
|
||
"List"
|
||
),
|
||
AdminPlantMutationRequest()
|
||
]);
|
||
|
||
AdminPlantCatalogue =
|
||
PlantResult.Plants ?? {};
|
||
|
||
AdminPlantMutationCatalogue =
|
||
MutationResult.Mutations ?? {};
|
||
|
||
Plants =
|
||
AdminPlantCatalogue;
|
||
|
||
MutationSets =
|
||
AdminPlantMutationCatalogue;
|
||
|
||
RenderAdminPlantSelect();
|
||
RenderAdminPlantDuplicateSelect();
|
||
|
||
const Keys =
|
||
GetSortedAdminPlantKeys();
|
||
|
||
const PlantKey =
|
||
PreferredPlantKey !== null &&
|
||
AdminPlantCatalogue[
|
||
PreferredPlantKey
|
||
] !== undefined
|
||
? PreferredPlantKey
|
||
: Keys[0] ?? null;
|
||
|
||
if (PlantKey === null) {
|
||
StartNewAdminPlant();
|
||
} else {
|
||
LoadAdminPlantIntoForm(
|
||
PlantKey
|
||
);
|
||
}
|
||
|
||
SetAdminPlantMessage(
|
||
Keys.length === 1
|
||
? "1 plant loaded."
|
||
: Keys.length +
|
||
" plants loaded."
|
||
);
|
||
}
|
||
|
||
|
||
function GetSortedAdminPlantKeys() {
|
||
return Object.keys(
|
||
AdminPlantCatalogue
|
||
).sort(
|
||
(A, B) => {
|
||
const PlantA =
|
||
AdminPlantCatalogue[A];
|
||
|
||
const PlantB =
|
||
AdminPlantCatalogue[B];
|
||
|
||
return (
|
||
Number(PlantA.Id) -
|
||
Number(PlantB.Id) ||
|
||
A.localeCompare(B)
|
||
);
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
function RenderAdminPlantSelect() {
|
||
const Select =
|
||
document.getElementById(
|
||
"AdminPlantSelect"
|
||
);
|
||
|
||
Select.replaceChildren();
|
||
|
||
for (
|
||
const PlantKey
|
||
of GetSortedAdminPlantKeys()
|
||
) {
|
||
const Plant =
|
||
AdminPlantCatalogue[
|
||
PlantKey
|
||
];
|
||
|
||
const Option =
|
||
document.createElement(
|
||
"option"
|
||
);
|
||
|
||
Option.value =
|
||
PlantKey;
|
||
|
||
Option.textContent =
|
||
String(
|
||
Plant.Id
|
||
).padStart(
|
||
3,
|
||
"0"
|
||
) +
|
||
" — " +
|
||
Plant.Name +
|
||
(
|
||
Plant.Archived === true
|
||
? " (archived)"
|
||
: ""
|
||
);
|
||
|
||
Select.appendChild(
|
||
Option
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
function RenderAdminPlantDuplicateSelect() {
|
||
const Select =
|
||
document.getElementById(
|
||
"AdminPlantDuplicateSelect"
|
||
);
|
||
|
||
if (Select === null) {
|
||
return;
|
||
}
|
||
|
||
const PreviousValue =
|
||
Select.value;
|
||
|
||
Select.replaceChildren();
|
||
|
||
for (
|
||
const PlantKey
|
||
of GetSortedAdminPlantKeys()
|
||
) {
|
||
const Plant =
|
||
AdminPlantCatalogue[
|
||
PlantKey
|
||
];
|
||
|
||
const Option =
|
||
document.createElement(
|
||
"option"
|
||
);
|
||
|
||
Option.value =
|
||
PlantKey;
|
||
|
||
Option.textContent =
|
||
String(
|
||
Plant.Id
|
||
).padStart(
|
||
3,
|
||
"0"
|
||
) +
|
||
" — " +
|
||
Plant.Name +
|
||
(
|
||
Plant.Archived === true
|
||
? " (archived)"
|
||
: ""
|
||
);
|
||
|
||
Select.appendChild(
|
||
Option
|
||
);
|
||
}
|
||
|
||
if (
|
||
PreviousValue !== "" &&
|
||
AdminPlantCatalogue[
|
||
PreviousValue
|
||
] !== undefined
|
||
) {
|
||
Select.value =
|
||
PreviousValue;
|
||
}
|
||
}
|
||
|
||
|
||
function LoadAdminPlantIntoForm(
|
||
PlantKey
|
||
) {
|
||
const Plant =
|
||
AdminPlantCatalogue[
|
||
PlantKey
|
||
];
|
||
|
||
if (Plant === undefined) {
|
||
return;
|
||
}
|
||
|
||
AdminPlantEditingKey =
|
||
PlantKey;
|
||
|
||
document.getElementById(
|
||
"AdminPlantSelect"
|
||
).value = PlantKey;
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantId",
|
||
Plant.Id
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantKey",
|
||
PlantKey
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantName",
|
||
Plant.Name ?? ""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantDescription",
|
||
Plant.Description ?? ""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantTags",
|
||
Array.isArray(
|
||
Plant.Tags
|
||
)
|
||
? Plant.Tags.join(", ")
|
||
: ""
|
||
);
|
||
|
||
SetAdminPlantDurationFields(
|
||
Number(
|
||
Plant.GrowthTime ?? 0
|
||
)
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantHarvestMultiplier",
|
||
Number(
|
||
Plant.HarvestMultiplier ??
|
||
1.5
|
||
)
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantShopPlant"
|
||
).checked =
|
||
Plant.Shop?.ShopPlant === true;
|
||
|
||
document.getElementById(
|
||
"AdminPlantDirectionalSprites"
|
||
).checked =
|
||
Plant.DirectionalSprites === true;
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantBaseCost",
|
||
Plant.Shop?.BaseCost ?? ""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantEffects",
|
||
JSON.stringify(
|
||
Plant.Effects ?? {},
|
||
null,
|
||
4
|
||
)
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantId"
|
||
).readOnly = true;
|
||
|
||
document.getElementById(
|
||
"AdminPlantKey"
|
||
).readOnly = true;
|
||
|
||
document.getElementById(
|
||
"AdminPlantArchiveNote"
|
||
).hidden =
|
||
Plant.Archived !== true;
|
||
|
||
UpdateAdminPlantShopFields();
|
||
RenderAdminPlantPreview();
|
||
}
|
||
|
||
|
||
function DuplicateAdminPlant() {
|
||
const SourcePlantKey =
|
||
document.getElementById(
|
||
"AdminPlantDuplicateSelect"
|
||
).value;
|
||
|
||
const SourcePlant =
|
||
AdminPlantCatalogue[
|
||
SourcePlantKey
|
||
];
|
||
|
||
if (SourcePlant === undefined) {
|
||
SetAdminPlantMessage(
|
||
"Choose a plant to duplicate."
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
AdminPlantEditingKey = null;
|
||
|
||
document.getElementById(
|
||
"AdminPlantSelect"
|
||
).value = "";
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantId",
|
||
GetNextAdminPlantId()
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantKey",
|
||
""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantName",
|
||
""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantDescription",
|
||
SourcePlant.Description ?? ""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantTags",
|
||
Array.isArray(
|
||
SourcePlant.Tags
|
||
)
|
||
? SourcePlant.Tags.join(
|
||
", "
|
||
)
|
||
: ""
|
||
);
|
||
|
||
SetAdminPlantDurationFields(
|
||
Number(
|
||
SourcePlant.GrowthTime ??
|
||
0
|
||
)
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantHarvestMultiplier",
|
||
Number(
|
||
SourcePlant.HarvestMultiplier ??
|
||
1.5
|
||
)
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantShopPlant"
|
||
).checked =
|
||
SourcePlant.Shop?.ShopPlant === true;
|
||
|
||
document.getElementById(
|
||
"AdminPlantDirectionalSprites"
|
||
).checked =
|
||
SourcePlant.DirectionalSprites === true;
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantBaseCost",
|
||
SourcePlant.Shop?.BaseCost ?? ""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantEffects",
|
||
JSON.stringify(
|
||
SourcePlant.Effects ?? {},
|
||
null,
|
||
4
|
||
)
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantId"
|
||
).readOnly = false;
|
||
|
||
document.getElementById(
|
||
"AdminPlantKey"
|
||
).readOnly = false;
|
||
|
||
document.getElementById(
|
||
"AdminPlantArchiveNote"
|
||
).hidden = true;
|
||
|
||
UpdateAdminPlantShopFields();
|
||
RenderAdminPlantPreview();
|
||
|
||
document.getElementById(
|
||
"AdminPlantName"
|
||
).focus();
|
||
|
||
SetAdminPlantMessage(
|
||
"Duplicated " +
|
||
SourcePlant.Name +
|
||
". Give the new plant a name and Plant Key before saving. Sprites are not copied."
|
||
);
|
||
}
|
||
|
||
|
||
function StartNewAdminPlant() {
|
||
AdminPlantEditingKey = null;
|
||
|
||
document.getElementById(
|
||
"AdminPlantSelect"
|
||
).value = "";
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantId",
|
||
GetNextAdminPlantId()
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantKey",
|
||
""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantName",
|
||
""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantDescription",
|
||
""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantTags",
|
||
""
|
||
);
|
||
|
||
SetAdminPlantDurationFields(
|
||
3600000
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantHarvestMultiplier",
|
||
1.5
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantShopPlant"
|
||
).checked = false;
|
||
|
||
document.getElementById(
|
||
"AdminPlantDirectionalSprites"
|
||
).checked = false;
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantBaseCost",
|
||
""
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantEffects",
|
||
"{}"
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantId"
|
||
).readOnly = false;
|
||
|
||
document.getElementById(
|
||
"AdminPlantKey"
|
||
).readOnly = false;
|
||
|
||
document.getElementById(
|
||
"AdminPlantArchiveNote"
|
||
).hidden = true;
|
||
|
||
UpdateAdminPlantShopFields();
|
||
RenderAdminPlantPreview();
|
||
SetAdminPlantMessage(
|
||
"Creating a new plant."
|
||
);
|
||
}
|
||
|
||
|
||
function GetNextAdminPlantId() {
|
||
return Math.max(
|
||
0,
|
||
...Object.values(
|
||
AdminPlantCatalogue
|
||
).map(
|
||
Plant =>
|
||
Number(
|
||
Plant.Id ?? 0
|
||
)
|
||
)
|
||
) + 1;
|
||
}
|
||
|
||
|
||
function SetAdminPlantField(
|
||
ElementId,
|
||
Value
|
||
) {
|
||
document.getElementById(
|
||
ElementId
|
||
).value =
|
||
Value ?? "";
|
||
}
|
||
|
||
|
||
function UpdateAdminPlantShopFields() {
|
||
const ShopPlant =
|
||
document.getElementById(
|
||
"AdminPlantShopPlant"
|
||
).checked;
|
||
|
||
const BaseCost =
|
||
document.getElementById(
|
||
"AdminPlantBaseCost"
|
||
);
|
||
|
||
BaseCost.disabled =
|
||
!ShopPlant;
|
||
|
||
BaseCost.required =
|
||
ShopPlant;
|
||
}
|
||
|
||
|
||
function ParseAdminPlantTags() {
|
||
const Value =
|
||
document.getElementById(
|
||
"AdminPlantTags"
|
||
).value;
|
||
|
||
return [
|
||
...new Set(
|
||
Value
|
||
.split(",")
|
||
.map(
|
||
Tag =>
|
||
Tag.trim()
|
||
)
|
||
.filter(
|
||
Tag =>
|
||
Tag.length > 0
|
||
)
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
function ParseAdminPlantEffects() {
|
||
const Value =
|
||
document.getElementById(
|
||
"AdminPlantEffects"
|
||
).value.trim();
|
||
|
||
if (Value === "") {
|
||
return {};
|
||
}
|
||
|
||
const Effects =
|
||
JSON.parse(Value);
|
||
|
||
if (
|
||
Effects === null ||
|
||
typeof Effects !== "object" ||
|
||
Array.isArray(Effects)
|
||
) {
|
||
throw new Error(
|
||
"Effects JSON must be an object."
|
||
);
|
||
}
|
||
|
||
return Effects;
|
||
}
|
||
|
||
|
||
function GetAdminPlantFormData() {
|
||
const ShopPlant =
|
||
document.getElementById(
|
||
"AdminPlantShopPlant"
|
||
).checked;
|
||
|
||
const BaseCostValue =
|
||
document.getElementById(
|
||
"AdminPlantBaseCost"
|
||
).value;
|
||
|
||
return {
|
||
Id: Number(
|
||
document.getElementById(
|
||
"AdminPlantId"
|
||
).value
|
||
),
|
||
|
||
PlantKey:
|
||
document.getElementById(
|
||
"AdminPlantKey"
|
||
).value.trim(),
|
||
|
||
Name:
|
||
document.getElementById(
|
||
"AdminPlantName"
|
||
).value.trim(),
|
||
|
||
Description:
|
||
document.getElementById(
|
||
"AdminPlantDescription"
|
||
).value.trim(),
|
||
|
||
Tags:
|
||
ParseAdminPlantTags(),
|
||
|
||
GrowthTime: Number(
|
||
document.getElementById(
|
||
"AdminPlantGrowthTime"
|
||
).value
|
||
),
|
||
|
||
HarvestMultiplier: Number(
|
||
document.getElementById(
|
||
"AdminPlantHarvestMultiplier"
|
||
).value
|
||
),
|
||
|
||
Effects:
|
||
ParseAdminPlantEffects(),
|
||
|
||
DirectionalSprites:
|
||
document.getElementById(
|
||
"AdminPlantDirectionalSprites"
|
||
).checked,
|
||
|
||
ShopPlant: ShopPlant,
|
||
|
||
BaseCost:
|
||
ShopPlant
|
||
? Number(BaseCostValue)
|
||
: null,
|
||
|
||
IsNew:
|
||
AdminPlantEditingKey ===
|
||
null
|
||
};
|
||
}
|
||
|
||
|
||
async function SaveAdminPlant(
|
||
Event
|
||
) {
|
||
Event.preventDefault();
|
||
|
||
if (AdminPlantSavePending) {
|
||
return;
|
||
}
|
||
|
||
const Form =
|
||
document.getElementById(
|
||
"AdminPlantForm"
|
||
);
|
||
|
||
if (!Form.reportValidity()) {
|
||
return;
|
||
}
|
||
|
||
let Plant;
|
||
|
||
try {
|
||
Plant =
|
||
GetAdminPlantFormData();
|
||
} catch (Error) {
|
||
SetAdminPlantMessage(
|
||
Error.message
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
try {
|
||
ValidateAdminPlant(
|
||
Plant,
|
||
AdminPlantCatalogue,
|
||
AdminPlantEditingKey
|
||
);
|
||
} catch (Error) {
|
||
SetAdminPlantMessage(
|
||
Error.message
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
AdminPlantSavePending = true;
|
||
|
||
const SaveButton =
|
||
document.getElementById(
|
||
"AdminPlantSaveButton"
|
||
);
|
||
|
||
SaveButton.disabled = true;
|
||
SaveButton.textContent =
|
||
"Saving...";
|
||
|
||
SetAdminPlantMessage(
|
||
"Saving plant..."
|
||
);
|
||
|
||
try {
|
||
const Result =
|
||
await AdminPlantRequest(
|
||
"Save",
|
||
{
|
||
Plant: Plant
|
||
}
|
||
);
|
||
|
||
localStorage.removeItem(
|
||
"SarahtoninGardenContent"
|
||
);
|
||
|
||
await LoadAdminPlants(
|
||
Result.PlantKey ??
|
||
Plant.PlantKey
|
||
);
|
||
|
||
SetAdminPlantMessage(
|
||
"Plant saved."
|
||
);
|
||
} catch (Error) {
|
||
console.error(
|
||
"Couldn't save plant:",
|
||
Error
|
||
);
|
||
|
||
SetAdminPlantMessage(
|
||
Error.message ??
|
||
"Couldn't save plant."
|
||
);
|
||
} finally {
|
||
AdminPlantSavePending = false;
|
||
SaveButton.disabled = false;
|
||
SaveButton.textContent =
|
||
"Save plant";
|
||
}
|
||
}
|
||
|
||
|
||
function RenderAdminPlantPreview() {
|
||
const PlantKey =
|
||
document.getElementById(
|
||
"AdminPlantKey"
|
||
).value.trim();
|
||
|
||
const Name =
|
||
document.getElementById(
|
||
"AdminPlantName"
|
||
).value.trim() ||
|
||
"New plant";
|
||
|
||
const Tile =
|
||
document.getElementById(
|
||
"AdminPlantPreviewTile"
|
||
);
|
||
|
||
Tile.replaceChildren();
|
||
|
||
const MatureImage =
|
||
GetPlantMatureImageSource(
|
||
AdminPlantCatalogue[
|
||
PlantKey
|
||
] ?? PlantKey
|
||
);
|
||
|
||
if (MatureImage !== null) {
|
||
const Image =
|
||
document.createElement(
|
||
"img"
|
||
);
|
||
|
||
Image.className =
|
||
"PlantSprite";
|
||
|
||
Image.src =
|
||
MatureImage;
|
||
|
||
Image.alt = Name;
|
||
|
||
Tile.appendChild(
|
||
Image
|
||
);
|
||
} else {
|
||
const Missing =
|
||
document.createElement(
|
||
"span"
|
||
);
|
||
|
||
Missing.className =
|
||
"AdminPlantPreviewMissing";
|
||
|
||
Missing.textContent =
|
||
PlantKey === ""
|
||
? "No key"
|
||
: "No sprite";
|
||
|
||
Tile.appendChild(
|
||
Missing
|
||
);
|
||
}
|
||
|
||
document.getElementById(
|
||
"AdminPlantPreviewName"
|
||
).textContent = Name;
|
||
|
||
document.getElementById(
|
||
"AdminPlantPreviewKey"
|
||
).textContent =
|
||
PlantKey === ""
|
||
? "Plant Key not set"
|
||
: PlantKey;
|
||
|
||
const GrowthTime = Number(
|
||
document.getElementById(
|
||
"AdminPlantGrowthTime"
|
||
).value
|
||
);
|
||
|
||
document.getElementById(
|
||
"AdminPlantGrowthTimeHint"
|
||
).textContent =
|
||
Number.isFinite(GrowthTime)
|
||
? GrowthTime.toLocaleString() +
|
||
" ms stored"
|
||
: "";
|
||
|
||
RenderAdminPlantEconomyPreview();
|
||
RenderAdminPlantImageManager();
|
||
}
|
||
|
||
|
||
function SetAdminPlantDurationFields(
|
||
Milliseconds
|
||
) {
|
||
Milliseconds = Math.max(
|
||
0,
|
||
Number(Milliseconds) || 0
|
||
);
|
||
|
||
const TotalSeconds =
|
||
Math.round(
|
||
Milliseconds / 1000
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantGrowthHours",
|
||
Math.floor(
|
||
TotalSeconds / 3600
|
||
)
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantGrowthMinutes",
|
||
Math.floor(
|
||
(TotalSeconds % 3600) / 60
|
||
)
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantGrowthSeconds",
|
||
TotalSeconds % 60
|
||
);
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantGrowthTime",
|
||
TotalSeconds * 1000
|
||
);
|
||
}
|
||
|
||
|
||
function SyncAdminPlantGrowthTime() {
|
||
const Hours = Number(
|
||
document.getElementById(
|
||
"AdminPlantGrowthHours"
|
||
).value
|
||
);
|
||
|
||
const Minutes = Number(
|
||
document.getElementById(
|
||
"AdminPlantGrowthMinutes"
|
||
).value
|
||
);
|
||
|
||
const Seconds = Number(
|
||
document.getElementById(
|
||
"AdminPlantGrowthSeconds"
|
||
).value
|
||
);
|
||
|
||
const Values = [
|
||
Hours,
|
||
Minutes,
|
||
Seconds
|
||
];
|
||
|
||
if (
|
||
Values.some(
|
||
Value =>
|
||
!Number.isFinite(Value)
|
||
)
|
||
) {
|
||
SetAdminPlantField(
|
||
"AdminPlantGrowthTime",
|
||
""
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
SetAdminPlantField(
|
||
"AdminPlantGrowthTime",
|
||
Math.max(0, Hours) * 3600000 +
|
||
Math.max(0, Minutes) * 60000 +
|
||
Math.max(0, Seconds) * 1000
|
||
);
|
||
}
|
||
|
||
|
||
function GetAdminPlantPreviewDefinition() {
|
||
const PlantId = Number(
|
||
document.getElementById(
|
||
"AdminPlantId"
|
||
).value
|
||
);
|
||
|
||
const PlantKey =
|
||
document.getElementById(
|
||
"AdminPlantKey"
|
||
).value.trim();
|
||
|
||
const Existing =
|
||
AdminPlantCatalogue[
|
||
PlantKey
|
||
] ?? {};
|
||
|
||
const ShopPlant =
|
||
document.getElementById(
|
||
"AdminPlantShopPlant"
|
||
).checked;
|
||
|
||
const BaseCostValue =
|
||
document.getElementById(
|
||
"AdminPlantBaseCost"
|
||
).value;
|
||
|
||
return {
|
||
...Existing,
|
||
Id: PlantId,
|
||
Name:
|
||
document.getElementById(
|
||
"AdminPlantName"
|
||
).value.trim() ||
|
||
"New plant",
|
||
Tags: ParseAdminPlantTags(),
|
||
GrowthTime: Number(
|
||
document.getElementById(
|
||
"AdminPlantGrowthTime"
|
||
).value
|
||
),
|
||
HarvestMultiplier: Number(
|
||
document.getElementById(
|
||
"AdminPlantHarvestMultiplier"
|
||
).value
|
||
),
|
||
DirectionalSprites:
|
||
document.getElementById(
|
||
"AdminPlantDirectionalSprites"
|
||
).checked,
|
||
Shop: {
|
||
ShopPlant: ShopPlant,
|
||
BaseCost:
|
||
ShopPlant &&
|
||
BaseCostValue !== ""
|
||
? Number(BaseCostValue)
|
||
: null
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
function RenderAdminPlantEconomyPreview() {
|
||
const Plant =
|
||
GetAdminPlantPreviewDefinition();
|
||
|
||
const PlantKey =
|
||
document.getElementById(
|
||
"AdminPlantKey"
|
||
).value.trim();
|
||
|
||
const PreviewCatalogue = {
|
||
...AdminPlantCatalogue,
|
||
[
|
||
PlantKey ||
|
||
"__AdminPlantPreview"
|
||
]: Plant
|
||
};
|
||
|
||
const CostInfo =
|
||
Number.isFinite(Plant.Id) &&
|
||
Plant.Id > 0
|
||
? GetCataloguePlantCostInfo(
|
||
Plant.Id,
|
||
PreviewCatalogue,
|
||
AdminPlantMutationCatalogue
|
||
)
|
||
: null;
|
||
|
||
const Multiplier =
|
||
GetPlantHarvestMultiplier(
|
||
Plant
|
||
);
|
||
|
||
const GrowthTime = Number(
|
||
Plant.GrowthTime
|
||
);
|
||
|
||
const Cost =
|
||
CostInfo?.Cost ?? null;
|
||
|
||
const Reward =
|
||
Cost === null
|
||
? null
|
||
: Math.ceil(
|
||
Cost * Multiplier
|
||
);
|
||
|
||
const Profit =
|
||
Cost === null ||
|
||
Reward === null
|
||
? null
|
||
: Reward - Cost;
|
||
|
||
const Dph =
|
||
Profit === null ||
|
||
!Number.isFinite(GrowthTime) ||
|
||
GrowthTime <= 0
|
||
? null
|
||
: Profit * 3600000 /
|
||
GrowthTime;
|
||
|
||
SetAdminPlantEconomyText(
|
||
"AdminPlantEconomyCost",
|
||
FormatAdminPlantDew(Cost)
|
||
);
|
||
|
||
SetAdminPlantEconomyText(
|
||
"AdminPlantEconomyMultiplier",
|
||
Multiplier.toLocaleString(
|
||
undefined,
|
||
{maximumFractionDigits: 4}
|
||
) + "×"
|
||
);
|
||
|
||
SetAdminPlantEconomyText(
|
||
"AdminPlantEconomyReward",
|
||
FormatAdminPlantDew(Reward)
|
||
);
|
||
|
||
SetAdminPlantEconomyText(
|
||
"AdminPlantEconomyProfit",
|
||
FormatAdminPlantDew(Profit)
|
||
);
|
||
|
||
SetAdminPlantEconomyText(
|
||
"AdminPlantEconomyGrowth",
|
||
Number.isFinite(GrowthTime)
|
||
? FormatAdminDuration(
|
||
GrowthTime
|
||
)
|
||
: "Unavailable"
|
||
);
|
||
|
||
SetAdminPlantEconomyText(
|
||
"AdminPlantEconomyDph",
|
||
Dph === null
|
||
? "Unavailable"
|
||
: Dph.toLocaleString(
|
||
undefined,
|
||
{maximumFractionDigits: 2}
|
||
) + " Dew/h"
|
||
);
|
||
|
||
let Source = "Unavailable";
|
||
|
||
if (CostInfo?.SourceType === "Base") {
|
||
Source = "Base seed cost";
|
||
} else if (
|
||
CostInfo?.SourceType ===
|
||
"Mutation"
|
||
) {
|
||
Source =
|
||
CostInfo.SourceMutation
|
||
?.Name ??
|
||
CostInfo.SourceMutation
|
||
?.MutationKey ??
|
||
"Mutation";
|
||
}
|
||
|
||
SetAdminPlantEconomyText(
|
||
"AdminPlantEconomySource",
|
||
Source
|
||
);
|
||
}
|
||
|
||
|
||
function SetAdminPlantEconomyText(
|
||
ElementId,
|
||
Value
|
||
) {
|
||
document.getElementById(
|
||
ElementId
|
||
).textContent = Value;
|
||
}
|
||
|
||
|
||
function FormatAdminPlantDew(
|
||
Value
|
||
) {
|
||
return (
|
||
Value === null ||
|
||
!Number.isFinite(Value)
|
||
)
|
||
? "Unavailable"
|
||
: Math.ceil(Value)
|
||
.toLocaleString() +
|
||
" Dew";
|
||
}
|
||
|
||
|
||
function RenderAdminPlantImageManager() {
|
||
const List =
|
||
document.getElementById(
|
||
"AdminPlantImageList"
|
||
);
|
||
|
||
const Note =
|
||
document.getElementById(
|
||
"AdminPlantImageNote"
|
||
);
|
||
|
||
const UploadButton =
|
||
document.getElementById(
|
||
"AdminPlantImageUploadButton"
|
||
);
|
||
|
||
const DefaultUpload =
|
||
document.getElementById(
|
||
"AdminPlantDefaultImageUpload"
|
||
);
|
||
|
||
const DirectionalUpload =
|
||
document.getElementById(
|
||
"AdminPlantDirectionalImageUpload"
|
||
);
|
||
|
||
const DirectionalToggle =
|
||
document.getElementById(
|
||
"AdminPlantDirectionalSprites"
|
||
);
|
||
|
||
const Inputs = [
|
||
"AdminPlantImageFiles",
|
||
"AdminPlantImageFilesNorth",
|
||
"AdminPlantImageFilesEast",
|
||
"AdminPlantImageFilesSouth",
|
||
"AdminPlantImageFilesWest"
|
||
]
|
||
.map(
|
||
ElementId =>
|
||
document.getElementById(
|
||
ElementId
|
||
)
|
||
)
|
||
.filter(Boolean);
|
||
|
||
if (
|
||
List === null ||
|
||
Note === null ||
|
||
UploadButton === null ||
|
||
DefaultUpload === null ||
|
||
DirectionalUpload === null ||
|
||
DirectionalToggle === null
|
||
) {
|
||
return;
|
||
}
|
||
|
||
List.replaceChildren();
|
||
|
||
const Plant =
|
||
AdminPlantEditingKey === null
|
||
? null
|
||
: AdminPlantCatalogue[
|
||
AdminPlantEditingKey
|
||
] ?? null;
|
||
|
||
const IsDirectional =
|
||
DirectionalToggle.checked;
|
||
|
||
DefaultUpload.hidden =
|
||
IsDirectional;
|
||
|
||
DirectionalUpload.hidden =
|
||
!IsDirectional;
|
||
|
||
const CanEdit =
|
||
Plant !== null &&
|
||
!AdminPlantImagePending;
|
||
|
||
UploadButton.disabled =
|
||
!CanEdit;
|
||
|
||
for (const Input of Inputs) {
|
||
Input.disabled = !CanEdit;
|
||
}
|
||
|
||
if (Plant === null) {
|
||
Note.textContent =
|
||
"Save the plant first, then upload sprites whenever you have them.";
|
||
return;
|
||
}
|
||
|
||
const StaticImages =
|
||
typeof PlantImages !== "undefined" &&
|
||
Array.isArray(
|
||
PlantImages[
|
||
AdminPlantEditingKey
|
||
]
|
||
)
|
||
? PlantImages[
|
||
AdminPlantEditingKey
|
||
]
|
||
: [];
|
||
|
||
const ApiStages =
|
||
Array.isArray(
|
||
Plant.ImageStages
|
||
)
|
||
? [...Plant.ImageStages]
|
||
.sort(
|
||
(A, B) =>
|
||
Number(A.Stage) -
|
||
Number(B.Stage)
|
||
)
|
||
: [];
|
||
|
||
const DirectionOrder = {
|
||
N: 0,
|
||
E: 1,
|
||
S: 2,
|
||
W: 3
|
||
};
|
||
|
||
const DirectionNames = {
|
||
N: "North",
|
||
E: "East",
|
||
S: "South",
|
||
W: "West"
|
||
};
|
||
|
||
const DirectionalStages =
|
||
Array.isArray(
|
||
Plant.DirectionalImageStages
|
||
)
|
||
? [...Plant.DirectionalImageStages]
|
||
.sort(
|
||
(A, B) =>
|
||
Number(A.Stage) -
|
||
Number(B.Stage) ||
|
||
(
|
||
DirectionOrder[
|
||
String(A.Direction)
|
||
.toUpperCase()
|
||
] ?? 99
|
||
) -
|
||
(
|
||
DirectionOrder[
|
||
String(B.Direction)
|
||
.toUpperCase()
|
||
] ?? 99
|
||
)
|
||
)
|
||
: [];
|
||
|
||
const FallbackText =
|
||
StaticImages.length > 0
|
||
? " Repository fallback: " +
|
||
StaticImages.length +
|
||
" stage" +
|
||
(
|
||
StaticImages.length === 1
|
||
? "."
|
||
: "s."
|
||
)
|
||
: "";
|
||
|
||
const DisplayStages =
|
||
IsDirectional
|
||
? DirectionalStages
|
||
: ApiStages;
|
||
|
||
if (DisplayStages.length === 0) {
|
||
Note.textContent =
|
||
IsDirectional
|
||
? "No directional API sprites uploaded. Choose stage-numbered PNGs for each direction." +
|
||
FallbackText
|
||
: "No API sprites uploaded." +
|
||
FallbackText;
|
||
return;
|
||
}
|
||
|
||
Note.textContent =
|
||
DisplayStages.length +
|
||
(IsDirectional
|
||
? " directional API sprite"
|
||
: " API sprite") +
|
||
(
|
||
DisplayStages.length === 1
|
||
? " uploaded."
|
||
: "s uploaded."
|
||
) +
|
||
FallbackText;
|
||
|
||
for (const ImageStage of DisplayStages) {
|
||
const Stage = Number(
|
||
ImageStage.Stage
|
||
);
|
||
|
||
const Direction =
|
||
IsDirectional
|
||
? String(
|
||
ImageStage.Direction ?? ""
|
||
).toUpperCase()
|
||
: null;
|
||
|
||
const Row =
|
||
document.createElement(
|
||
"div"
|
||
);
|
||
|
||
Row.className =
|
||
"AdminPlantImageRow";
|
||
|
||
const Preview =
|
||
document.createElement(
|
||
"div"
|
||
);
|
||
|
||
Preview.className =
|
||
"PlantTile AdminPlantImagePreview";
|
||
|
||
const Image =
|
||
document.createElement(
|
||
"img"
|
||
);
|
||
|
||
Image.className =
|
||
"PlantSprite";
|
||
|
||
Image.src =
|
||
GetAdminPlantApiImageUrl(
|
||
Plant.Id,
|
||
Stage,
|
||
ImageStage.Revision,
|
||
Direction
|
||
);
|
||
|
||
Image.alt =
|
||
Plant.Name +
|
||
" stage " +
|
||
Stage +
|
||
(
|
||
Direction === null
|
||
? ""
|
||
: " " +
|
||
(
|
||
DirectionNames[
|
||
Direction
|
||
] ?? Direction
|
||
)
|
||
);
|
||
|
||
Preview.appendChild(Image);
|
||
|
||
const Label =
|
||
document.createElement(
|
||
"strong"
|
||
);
|
||
|
||
Label.textContent =
|
||
"Stage " + Stage +
|
||
(
|
||
Direction === null
|
||
? ""
|
||
: " · " +
|
||
(
|
||
DirectionNames[
|
||
Direction
|
||
] ?? Direction
|
||
)
|
||
);
|
||
|
||
const RemoveButton =
|
||
document.createElement(
|
||
"button"
|
||
);
|
||
|
||
RemoveButton.type = "button";
|
||
RemoveButton.className =
|
||
"ActionButton AdminInlineButton AdminPlantImageRemoveButton";
|
||
RemoveButton.textContent =
|
||
"Remove";
|
||
RemoveButton.disabled =
|
||
AdminPlantImagePending;
|
||
|
||
RemoveButton.addEventListener(
|
||
"click",
|
||
() =>
|
||
DeleteAdminPlantImage(
|
||
Plant.Id,
|
||
Stage,
|
||
Direction
|
||
)
|
||
);
|
||
|
||
Row.append(
|
||
Preview,
|
||
Label,
|
||
RemoveButton
|
||
);
|
||
|
||
List.appendChild(Row);
|
||
}
|
||
}
|
||
|
||
|
||
function GetAdminPlantApiImageUrl(
|
||
PlantId,
|
||
Stage,
|
||
Revision = 0,
|
||
Direction = null
|
||
) {
|
||
const QueryData = {
|
||
PlantId: String(PlantId),
|
||
Stage: String(Stage),
|
||
v: String(Revision ?? 0)
|
||
};
|
||
|
||
if (Direction !== null) {
|
||
QueryData.Direction =
|
||
String(Direction);
|
||
}
|
||
|
||
const Query =
|
||
new URLSearchParams(
|
||
QueryData
|
||
);
|
||
|
||
return ApiUrl +
|
||
"/PlantImage.php?" +
|
||
Query.toString();
|
||
}
|
||
|
||
|
||
function GetAdminPlantImageUploadSets() {
|
||
const IsDirectional =
|
||
document.getElementById(
|
||
"AdminPlantDirectionalSprites"
|
||
).checked;
|
||
|
||
if (!IsDirectional) {
|
||
return [
|
||
{
|
||
Direction: null,
|
||
Input:
|
||
document.getElementById(
|
||
"AdminPlantImageFiles"
|
||
)
|
||
}
|
||
];
|
||
}
|
||
|
||
return [
|
||
["N", "AdminPlantImageFilesNorth"],
|
||
["E", "AdminPlantImageFilesEast"],
|
||
["S", "AdminPlantImageFilesSouth"],
|
||
["W", "AdminPlantImageFilesWest"]
|
||
].map(
|
||
([Direction, ElementId]) => ({
|
||
Direction: Direction,
|
||
Input:
|
||
document.getElementById(
|
||
ElementId
|
||
)
|
||
})
|
||
);
|
||
}
|
||
|
||
|
||
async function UploadAdminPlantImages() {
|
||
if (
|
||
AdminPlantImagePending ||
|
||
AdminPlantEditingKey === null
|
||
) {
|
||
return;
|
||
}
|
||
|
||
const Plant =
|
||
AdminPlantCatalogue[
|
||
AdminPlantEditingKey
|
||
];
|
||
|
||
if (Plant === undefined) {
|
||
return;
|
||
}
|
||
|
||
const Uploads = [];
|
||
|
||
try {
|
||
for (
|
||
const UploadSet
|
||
of GetAdminPlantImageUploadSets()
|
||
) {
|
||
const Files = Array.from(
|
||
UploadSet.Input?.files ?? []
|
||
);
|
||
|
||
const Stages = new Set();
|
||
|
||
for (const File of Files) {
|
||
const Match =
|
||
/^(\d+)\.png$/i.exec(
|
||
File.name
|
||
);
|
||
|
||
if (Match === null) {
|
||
throw new Error(
|
||
"Sprite filenames must be stage numbers such as 1.png or 3.png."
|
||
);
|
||
}
|
||
|
||
const Stage = Number(
|
||
Match[1]
|
||
);
|
||
|
||
if (
|
||
!Number.isInteger(Stage) ||
|
||
Stage < 1 ||
|
||
Stage > 255
|
||
) {
|
||
throw new Error(
|
||
"Sprite stages must be between 1 and 255."
|
||
);
|
||
}
|
||
|
||
if (Stages.has(Stage)) {
|
||
throw new Error(
|
||
"Stage " + Stage +
|
||
" was selected more than once for the same direction."
|
||
);
|
||
}
|
||
|
||
Stages.add(Stage);
|
||
|
||
Uploads.push({
|
||
Stage: Stage,
|
||
Direction:
|
||
UploadSet.Direction,
|
||
File: File
|
||
});
|
||
}
|
||
}
|
||
} catch (Error) {
|
||
SetAdminPlantMessage(
|
||
Error.message
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (Uploads.length === 0) {
|
||
SetAdminPlantMessage(
|
||
"Choose one or more PNG sprites first."
|
||
);
|
||
return;
|
||
}
|
||
|
||
AdminPlantImagePending = true;
|
||
RenderAdminPlantImageManager();
|
||
|
||
SetAdminPlantMessage(
|
||
"Uploading sprites..."
|
||
);
|
||
|
||
try {
|
||
for (const Upload of Uploads) {
|
||
const DataBase64 =
|
||
await ReadAdminPlantFileBase64(
|
||
Upload.File
|
||
);
|
||
|
||
const RequestData = {
|
||
PlantId: Plant.Id,
|
||
Stage: Upload.Stage,
|
||
MimeType: "image/png",
|
||
DataBase64: DataBase64
|
||
};
|
||
|
||
if (Upload.Direction !== null) {
|
||
RequestData.Direction =
|
||
Upload.Direction;
|
||
}
|
||
|
||
await AdminPlantRequest(
|
||
"UploadImage",
|
||
RequestData
|
||
);
|
||
}
|
||
|
||
for (
|
||
const UploadSet
|
||
of GetAdminPlantImageUploadSets()
|
||
) {
|
||
if (UploadSet.Input !== null) {
|
||
UploadSet.Input.value = "";
|
||
}
|
||
}
|
||
|
||
localStorage.removeItem(
|
||
"SarahtoninGardenContent"
|
||
);
|
||
|
||
await LoadAdminPlants(
|
||
AdminPlantEditingKey
|
||
);
|
||
|
||
SetAdminPlantMessage(
|
||
Uploads.length === 1
|
||
? "Sprite uploaded."
|
||
: Uploads.length +
|
||
" sprites uploaded."
|
||
);
|
||
} catch (Error) {
|
||
console.error(
|
||
"Couldn't upload plant sprites:",
|
||
Error
|
||
);
|
||
|
||
SetAdminPlantMessage(
|
||
Error.message ??
|
||
"Couldn't upload plant sprites."
|
||
);
|
||
} finally {
|
||
AdminPlantImagePending = false;
|
||
RenderAdminPlantImageManager();
|
||
}
|
||
}
|
||
|
||
|
||
function ReadAdminPlantFileBase64(
|
||
File
|
||
) {
|
||
return new Promise(
|
||
(Resolve, Reject) => {
|
||
const Reader =
|
||
new FileReader();
|
||
|
||
Reader.addEventListener(
|
||
"load",
|
||
() => {
|
||
const Result =
|
||
String(
|
||
Reader.result ??
|
||
""
|
||
);
|
||
|
||
const CommaIndex =
|
||
Result.indexOf(",");
|
||
|
||
if (CommaIndex < 0) {
|
||
Reject(
|
||
new Error(
|
||
"Couldn't read sprite file."
|
||
)
|
||
);
|
||
return;
|
||
}
|
||
|
||
Resolve(
|
||
Result.slice(
|
||
CommaIndex + 1
|
||
)
|
||
);
|
||
}
|
||
);
|
||
|
||
Reader.addEventListener(
|
||
"error",
|
||
() =>
|
||
Reject(
|
||
new Error(
|
||
"Couldn't read sprite file."
|
||
)
|
||
)
|
||
);
|
||
|
||
Reader.readAsDataURL(File);
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
async function DeleteAdminPlantImage(
|
||
PlantId,
|
||
Stage,
|
||
Direction = null
|
||
) {
|
||
if (AdminPlantImagePending) {
|
||
return;
|
||
}
|
||
|
||
AdminPlantImagePending = true;
|
||
RenderAdminPlantImageManager();
|
||
|
||
SetAdminPlantMessage(
|
||
"Removing stage " +
|
||
Stage +
|
||
(
|
||
Direction === null
|
||
? ""
|
||
: " " + Direction
|
||
) +
|
||
" sprite..."
|
||
);
|
||
|
||
try {
|
||
const RequestData = {
|
||
PlantId: PlantId,
|
||
Stage: Stage
|
||
};
|
||
|
||
if (Direction !== null) {
|
||
RequestData.Direction =
|
||
Direction;
|
||
}
|
||
|
||
await AdminPlantRequest(
|
||
"DeleteImage",
|
||
RequestData
|
||
);
|
||
|
||
localStorage.removeItem(
|
||
"SarahtoninGardenContent"
|
||
);
|
||
|
||
await LoadAdminPlants(
|
||
AdminPlantEditingKey
|
||
);
|
||
|
||
SetAdminPlantMessage(
|
||
"Stage " +
|
||
Stage +
|
||
(
|
||
Direction === null
|
||
? ""
|
||
: " " + Direction
|
||
) +
|
||
" sprite removed."
|
||
);
|
||
} catch (Error) {
|
||
console.error(
|
||
"Couldn't remove plant sprite:",
|
||
Error
|
||
);
|
||
|
||
SetAdminPlantMessage(
|
||
Error.message ??
|
||
"Couldn't remove plant sprite."
|
||
);
|
||
} finally {
|
||
AdminPlantImagePending = false;
|
||
RenderAdminPlantImageManager();
|
||
}
|
||
}
|
||
|
||
|
||
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 ?? {}
|
||
)
|
||
),
|
||
DirectionalSprites:
|
||
Plant.DirectionalSprites === true,
|
||
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
|
||
)
|
||
)
|
||
: {},
|
||
DirectionalSprites:
|
||
Imported.DirectionalSprites ===
|
||
true,
|
||
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;
|
||
|
||
document.getElementById(
|
||
"AdminPlantDirectionalSprites"
|
||
).checked =
|
||
Plant.DirectionalSprites;
|
||
|
||
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
|
||
) {
|
||
Milliseconds = Math.max(
|
||
0,
|
||
Number(Milliseconds)
|
||
);
|
||
|
||
if (Milliseconds < 1000) {
|
||
return Milliseconds + "ms";
|
||
}
|
||
|
||
const TotalSeconds =
|
||
Math.round(
|
||
Milliseconds /
|
||
1000
|
||
);
|
||
|
||
const Hours =
|
||
Math.floor(
|
||
TotalSeconds /
|
||
3600
|
||
);
|
||
|
||
const Minutes =
|
||
Math.floor(
|
||
(
|
||
TotalSeconds %
|
||
3600
|
||
) /
|
||
60
|
||
);
|
||
|
||
const Seconds =
|
||
TotalSeconds % 60;
|
||
|
||
const Parts = [];
|
||
|
||
if (Hours > 0) {
|
||
Parts.push(
|
||
Hours + "h"
|
||
);
|
||
}
|
||
|
||
if (Minutes > 0) {
|
||
Parts.push(
|
||
Minutes + "m"
|
||
);
|
||
}
|
||
|
||
if (
|
||
Seconds > 0 ||
|
||
Parts.length === 0
|
||
) {
|
||
Parts.push(
|
||
Seconds + "s"
|
||
);
|
||
}
|
||
|
||
return Parts.join(" ");
|
||
}
|
||
|
||
|
||
function SetAdminPlantMessage(
|
||
Message
|
||
) {
|
||
const Element =
|
||
document.getElementById(
|
||
"AdminPlantMessage"
|
||
);
|
||
|
||
if (Element !== null) {
|
||
Element.textContent =
|
||
Message;
|
||
}
|
||
}
|
||
|
||
|
||
document.addEventListener(
|
||
"DOMContentLoaded",
|
||
StartAdminPlantEditor
|
||
);
|