Files

2484 lines
51 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
);