This commit is contained in:
sarah committed 2026-09-03 19:57:22 +02:00
1 parent 17c29270cb
commit b0bc6ecc03
13 files changed
+1809 -168

No files matched your search

+43 -7
View File
@@ -132,17 +132,53 @@ $PageSection = "Admin";
> >
</label> </label>
<label class="AdminEditorField"> <div class="AdminEditorField">
<span>Cooldown (ms)</span> <span>Cooldown</span>
<div class="AdminDurationInputs">
<label>
<span>Hours</span>
<input
id="AdminMutationCooldownHours"
type="number"
min="0"
step="1"
required
>
</label>
<label>
<span>Minutes</span>
<input
id="AdminMutationCooldownMinutes"
type="number"
min="0"
max="59"
step="1"
required
>
</label>
<label>
<span>Seconds</span>
<input
id="AdminMutationCooldownSeconds"
type="number"
min="0"
max="59"
step="1"
required
>
</label>
</div>
<input <input
id="AdminMutationCooldown" id="AdminMutationCooldown"
type="number" type="hidden"
min="0"
step="1000"
required
> >
<small id="AdminMutationCooldownHint"></small> <small id="AdminMutationCooldownHint"></small>
</label> </div>
<label class="AdminEditorField"> <label class="AdminEditorField">
<span>Rotation</span> <span>Rotation</span>
+131 -5
View File
@@ -80,7 +80,7 @@ $PageSection = "Admin";
required required
> >
<small> <small>
Used by saves, recipes and the sprite folder. Used by saves, recipes and repository sprite fallbacks.
</small> </small>
</label> </label>
@@ -118,17 +118,69 @@ $PageSection = "Admin";
</small> </small>
</label> </label>
<label class="AdminEditorField"> <div class="AdminEditorField">
<span>Growth time (ms)</span> <span>Growth time</span>
<div class="AdminDurationInputs">
<label>
<span>Hours</span>
<input
id="AdminPlantGrowthHours"
type="number"
min="0"
step="1"
required
>
</label>
<label>
<span>Minutes</span>
<input
id="AdminPlantGrowthMinutes"
type="number"
min="0"
max="59"
step="1"
required
>
</label>
<label>
<span>Seconds</span>
<input
id="AdminPlantGrowthSeconds"
type="number"
min="0"
max="59"
step="1"
required
>
</label>
</div>
<input <input
id="AdminPlantGrowthTime" id="AdminPlantGrowthTime"
name="GrowthTime" name="GrowthTime"
type="hidden"
>
<small id="AdminPlantGrowthTimeHint"></small>
</div>
<label class="AdminEditorField">
<span>Harvest multiplier</span>
<input
id="AdminPlantHarvestMultiplier"
name="HarvestMultiplier"
type="number" type="number"
min="0" min="0"
step="1000" max="9999.9999"
step="0.0001"
required required
> >
<small id="AdminPlantGrowthTimeHint"></small> <small>
Defaults to 1.5. Reward is seed cost × this multiplier.
</small>
</label> </label>
<label class="AdminEditorField"> <label class="AdminEditorField">
@@ -189,6 +241,78 @@ $PageSection = "Admin";
</p> </p>
</div> </div>
</div> </div>
<div class="AdminPlantEconomyPreview">
<h3>Economy</h3>
<dl>
<div>
<dt>Seed cost</dt>
<dd id="AdminPlantEconomyCost">Unavailable</dd>
</div>
<div>
<dt>Harvest multiplier</dt>
<dd id="AdminPlantEconomyMultiplier">1.5×</dd>
</div>
<div>
<dt>Harvest reward</dt>
<dd id="AdminPlantEconomyReward">Unavailable</dd>
</div>
<div>
<dt>Net profit</dt>
<dd id="AdminPlantEconomyProfit">Unavailable</dd>
</div>
<div>
<dt>Growth time</dt>
<dd id="AdminPlantEconomyGrowth">Unavailable</dd>
</div>
<div>
<dt>DPH</dt>
<dd id="AdminPlantEconomyDph">Unavailable</dd>
</div>
<div class="AdminPlantEconomySource">
<dt>Price source</dt>
<dd id="AdminPlantEconomySource">Unavailable</dd>
</div>
</dl>
</div>
</div>
<div class="AdminPlantImagesSection">
<h2>Sprites</h2>
<p>
Optional. You can save a plant with no sprites and add them later.
Upload PNG files named by stage, such as
<code>1.png</code>, <code>2.png</code>, <code>3.png</code>.
</p>
<div class="AdminPlantImageUpload">
<input
id="AdminPlantImageFiles"
type="file"
accept="image/png,.png"
multiple
>
<button
id="AdminPlantImageUploadButton"
class="ActionButton AdminInlineButton"
type="button"
>
Upload selected sprites
</button>
</div>
<p
id="AdminPlantImageNote"
class="AdminPlantImageNote"
></p>
<div
id="AdminPlantImageList"
class="AdminPlantImageList"
></div>
</div> </div>
<div class="AdminEditorActions"> <div class="AdminEditorActions">
@@ -219,7 +343,9 @@ $PageSection = "Admin";
<script src="/Scripts/Garden/PlantImages.js"></script> <script src="/Scripts/Garden/PlantImages.js"></script>
<script src="/Scripts/Garden/Plants.js"></script> <script src="/Scripts/Garden/Plants.js"></script>
<script src="/Scripts/Garden/Mutations.js"></script>
<script src="/Scripts/Garden/Save.js"></script> <script src="/Scripts/Garden/Save.js"></script>
<script src="/Scripts/Garden/Economy.js"></script>
<script src="/Scripts/Garden/AdminPlantEditor.js"></script> <script src="/Scripts/Garden/AdminPlantEditor.js"></script>
</body> </body>
</html> </html>
+131 -90
View File
@@ -175,12 +175,34 @@ function BindAdminMutationEditor() {
RenderAdminMutationJsonPreview RenderAdminMutationJsonPreview
); );
document.getElementById( for (
"AdminMutationCooldown" const ElementId
).addEventListener( of [
"input", "AdminMutationCooldownHours",
UpdateAdminMutationCooldownHint "AdminMutationCooldownMinutes",
); "AdminMutationCooldownSeconds"
]
) {
document.getElementById(
ElementId
).addEventListener(
"input",
() => {
SyncAdminMutationCooldown();
RenderAdminMutationJsonPreview();
}
);
document.getElementById(
ElementId
).addEventListener(
"change",
() => {
SyncAdminMutationCooldown();
RenderAdminMutationJsonPreview();
}
);
}
} }
@@ -645,8 +667,7 @@ function LoadAdminMutationIntoForm(
) * 100 ) * 100
); );
SetAdminMutationField( SetAdminMutationDurationFields(
"AdminMutationCooldown",
Number( Number(
Mutation.Cooldown ?? 0 Mutation.Cooldown ?? 0
) )
@@ -769,8 +790,7 @@ function StartNewAdminMutation() {
100 100
); );
SetAdminMutationField( SetAdminMutationDurationFields(
"AdminMutationCooldown",
120000 120000
); );
@@ -1403,21 +1423,11 @@ function GetAdminPlantName(
function GetAdminMutationPlantImage( function GetAdminMutationPlantImage(
PlantKey PlantKey
) { ) {
const Images = return GetPlantMatureImageSource(
PlantImages[ AdminMutationPlantCatalogue[
PlantKey PlantKey
] ?? []; ] ?? PlantKey
);
if (
!Array.isArray(Images) ||
Images.length === 0
) {
return null;
}
return Images[
Images.length - 1
];
} }
@@ -2179,6 +2189,101 @@ function RenderAdminMutationJsonPreview() {
} }
function SetAdminMutationDurationFields(
Milliseconds
) {
Milliseconds = Math.max(
0,
Number(Milliseconds) || 0
);
const TotalSeconds =
Math.round(
Milliseconds / 1000
);
SetAdminMutationField(
"AdminMutationCooldownHours",
Math.floor(
TotalSeconds / 3600
)
);
SetAdminMutationField(
"AdminMutationCooldownMinutes",
Math.floor(
(TotalSeconds % 3600) / 60
)
);
SetAdminMutationField(
"AdminMutationCooldownSeconds",
TotalSeconds % 60
);
SetAdminMutationField(
"AdminMutationCooldown",
TotalSeconds * 1000
);
UpdateAdminMutationCooldownHint();
}
function SyncAdminMutationCooldown() {
const Hours = Number(
document.getElementById(
"AdminMutationCooldownHours"
).value
);
const Minutes = Number(
document.getElementById(
"AdminMutationCooldownMinutes"
).value
);
const Seconds = Number(
document.getElementById(
"AdminMutationCooldownSeconds"
).value
);
const Values = [
Hours,
Minutes,
Seconds
];
if (
Values.some(
Value =>
!Number.isFinite(Value)
)
) {
SetAdminMutationField(
"AdminMutationCooldown",
""
);
UpdateAdminMutationCooldownHint();
return;
}
const Milliseconds =
Math.max(0, Hours) * 3600000 +
Math.max(0, Minutes) * 60000 +
Math.max(0, Seconds) * 1000;
SetAdminMutationField(
"AdminMutationCooldown",
Milliseconds
);
UpdateAdminMutationCooldownHint();
}
function UpdateAdminMutationCooldownHint() { function UpdateAdminMutationCooldownHint() {
const Cooldown = Number( const Cooldown = Number(
document.getElementById( document.getElementById(
@@ -2190,76 +2295,12 @@ function UpdateAdminMutationCooldownHint() {
"AdminMutationCooldownHint" "AdminMutationCooldownHint"
).textContent = ).textContent =
Number.isFinite(Cooldown) Number.isFinite(Cooldown)
? FormatAdminMutationDuration( ? Cooldown.toLocaleString() +
Cooldown " ms stored"
)
: ""; : "";
} }
function FormatAdminMutationDuration(
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 SetAdminMutationMessage( function SetAdminMutationMessage(
Message Message
) { ) {
+886 -21
View File
@@ -1,6 +1,8 @@
let AdminPlantCatalogue = {}; let AdminPlantCatalogue = {};
let AdminPlantMutationCatalogue = {};
let AdminPlantEditingKey = null; let AdminPlantEditingKey = null;
let AdminPlantSavePending = false; let AdminPlantSavePending = false;
let AdminPlantImagePending = false;
async function StartAdminPlantEditor() { async function StartAdminPlantEditor() {
@@ -75,7 +77,7 @@ function BindAdminPlantEditor() {
"AdminPlantId", "AdminPlantId",
"AdminPlantKey", "AdminPlantKey",
"AdminPlantName", "AdminPlantName",
"AdminPlantGrowthTime", "AdminPlantHarvestMultiplier",
"AdminPlantShopPlant", "AdminPlantShopPlant",
"AdminPlantBaseCost" "AdminPlantBaseCost"
] ]
@@ -95,11 +97,50 @@ function BindAdminPlantEditor() {
); );
} }
for (
const ElementId
of [
"AdminPlantGrowthHours",
"AdminPlantGrowthMinutes",
"AdminPlantGrowthSeconds"
]
) {
document.getElementById(
ElementId
).addEventListener(
"input",
() => {
SyncAdminPlantGrowthTime();
RenderAdminPlantPreview();
}
);
document.getElementById(
ElementId
).addEventListener(
"change",
() => {
SyncAdminPlantGrowthTime();
RenderAdminPlantPreview();
}
);
}
document.getElementById( document.getElementById(
"AdminPlantShopPlant" "AdminPlantShopPlant"
).addEventListener( ).addEventListener(
"change", "change",
UpdateAdminPlantShopFields () => {
UpdateAdminPlantShopFields();
RenderAdminPlantPreview();
}
);
document.getElementById(
"AdminPlantImageUploadButton"
).addEventListener(
"click",
UploadAdminPlantImages
); );
} }
@@ -167,6 +208,47 @@ async function AdminPlantRequest(
} }
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( async function LoadAdminPlants(
PreferredPlantKey = null PreferredPlantKey = null
) { ) {
@@ -174,17 +256,28 @@ async function LoadAdminPlants(
"Loading plants..." "Loading plants..."
); );
const Result = const [
await AdminPlantRequest( PlantResult,
MutationResult
] = await Promise.all([
AdminPlantRequest(
"List" "List"
); ),
AdminPlantMutationRequest()
]);
AdminPlantCatalogue = AdminPlantCatalogue =
Result.Plants ?? {}; PlantResult.Plants ?? {};
AdminPlantMutationCatalogue =
MutationResult.Mutations ?? {};
Plants = Plants =
AdminPlantCatalogue; AdminPlantCatalogue;
MutationSets =
AdminPlantMutationCatalogue;
RenderAdminPlantSelect(); RenderAdminPlantSelect();
const Keys = const Keys =
@@ -331,13 +424,20 @@ function LoadAdminPlantIntoForm(
: "" : ""
); );
SetAdminPlantField( SetAdminPlantDurationFields(
"AdminPlantGrowthTime",
Number( Number(
Plant.GrowthTime ?? 0 Plant.GrowthTime ?? 0
) )
); );
SetAdminPlantField(
"AdminPlantHarvestMultiplier",
Number(
Plant.HarvestMultiplier ??
1.5
)
);
document.getElementById( document.getElementById(
"AdminPlantShopPlant" "AdminPlantShopPlant"
).checked = ).checked =
@@ -407,11 +507,15 @@ function StartNewAdminPlant() {
"" ""
); );
SetAdminPlantField( SetAdminPlantDurationFields(
"AdminPlantGrowthTime",
3600000 3600000
); );
SetAdminPlantField(
"AdminPlantHarvestMultiplier",
1.5
);
document.getElementById( document.getElementById(
"AdminPlantShopPlant" "AdminPlantShopPlant"
).checked = false; ).checked = false;
@@ -583,6 +687,12 @@ function GetAdminPlantFormData() {
).value ).value
), ),
HarvestMultiplier: Number(
document.getElementById(
"AdminPlantHarvestMultiplier"
).value
),
Effects: Effects:
ParseAdminPlantEffects(), ParseAdminPlantEffects(),
@@ -705,15 +815,14 @@ function RenderAdminPlantPreview() {
Tile.replaceChildren(); Tile.replaceChildren();
const Images = const MatureImage =
PlantImages[ GetPlantMatureImageSource(
PlantKey AdminPlantCatalogue[
] ?? []; PlantKey
] ?? PlantKey
);
if ( if (MatureImage !== null) {
Array.isArray(Images) &&
Images.length > 0
) {
const Image = const Image =
document.createElement( document.createElement(
"img" "img"
@@ -723,9 +832,7 @@ function RenderAdminPlantPreview() {
"PlantSprite"; "PlantSprite";
Image.src = Image.src =
Images[ MatureImage;
Images.length - 1
];
Image.alt = Name; Image.alt = Name;
@@ -771,11 +878,769 @@ function RenderAdminPlantPreview() {
document.getElementById( document.getElementById(
"AdminPlantGrowthTimeHint" "AdminPlantGrowthTimeHint"
).textContent = ).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
),
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) Number.isFinite(GrowthTime)
? FormatAdminDuration( ? FormatAdminDuration(
GrowthTime 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 FileInput =
document.getElementById(
"AdminPlantImageFiles"
);
if (
List === null ||
Note === null ||
UploadButton === null ||
FileInput === null
) {
return;
}
List.replaceChildren();
const Plant =
AdminPlantEditingKey === null
? null
: AdminPlantCatalogue[
AdminPlantEditingKey
] ?? null;
const CanEdit =
Plant !== null &&
!AdminPlantImagePending;
UploadButton.disabled =
!CanEdit;
FileInput.disabled =
!CanEdit;
if (Plant === null) {
Note.textContent =
"Save the plant first, then upload sprites whenever you have them.";
return;
}
const StaticImages =
Array.isArray(
PlantImages[
AdminPlantEditingKey
]
)
? PlantImages[
AdminPlantEditingKey
]
: [];
const ApiStages =
Array.isArray(
Plant.ImageStages
)
? [...Plant.ImageStages]
.sort(
(A, B) =>
Number(A.Stage) -
Number(B.Stage)
)
: [];
const FallbackText =
StaticImages.length > 0
? " Repository fallback: " +
StaticImages.length +
" stage" +
(
StaticImages.length === 1
? "."
: "s."
)
: ""; : "";
if (ApiStages.length === 0) {
Note.textContent =
"No API sprites uploaded." +
FallbackText;
return;
}
Note.textContent =
ApiStages.length +
" API sprite" +
(
ApiStages.length === 1
? " uploaded."
: "s uploaded."
) +
FallbackText;
for (const ImageStage of ApiStages) {
const Stage = Number(
ImageStage.Stage
);
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
);
Image.alt =
Plant.Name +
" stage " +
Stage;
Preview.appendChild(Image);
const Label =
document.createElement(
"strong"
);
Label.textContent =
"Stage " + Stage;
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
)
);
Row.append(
Preview,
Label,
RemoveButton
);
List.appendChild(Row);
}
}
function GetAdminPlantApiImageUrl(
PlantId,
Stage,
Revision = 0
) {
const Query =
new URLSearchParams({
PlantId: String(PlantId),
Stage: String(Stage),
v: String(Revision ?? 0)
});
return ApiUrl +
"/PlantImage.php?" +
Query.toString();
}
async function UploadAdminPlantImages() {
if (
AdminPlantImagePending ||
AdminPlantEditingKey === null
) {
return;
}
const Plant =
AdminPlantCatalogue[
AdminPlantEditingKey
];
if (Plant === undefined) {
return;
}
const FileInput =
document.getElementById(
"AdminPlantImageFiles"
);
const Files =
Array.from(
FileInput.files ?? []
);
if (Files.length === 0) {
SetAdminPlantMessage(
"Choose one or more PNG sprites first."
);
return;
}
const Stages = new Set();
const Uploads = [];
try {
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."
);
}
Stages.add(Stage);
Uploads.push({
Stage: Stage,
File: File
});
}
} catch (Error) {
SetAdminPlantMessage(
Error.message
);
return;
}
AdminPlantImagePending = true;
RenderAdminPlantImageManager();
SetAdminPlantMessage(
"Uploading sprites..."
);
try {
for (const Upload of Uploads) {
const DataBase64 =
await ReadAdminPlantFileBase64(
Upload.File
);
await AdminPlantRequest(
"UploadImage",
{
PlantId: Plant.Id,
Stage: Upload.Stage,
MimeType: "image/png",
DataBase64: DataBase64
}
);
}
FileInput.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
) {
if (AdminPlantImagePending) {
return;
}
AdminPlantImagePending = true;
RenderAdminPlantImageManager();
SetAdminPlantMessage(
"Removing stage " +
Stage +
" sprite..."
);
try {
await AdminPlantRequest(
"DeleteImage",
{
PlantId: PlantId,
Stage: Stage
}
);
localStorage.removeItem(
"SarahtoninGardenContent"
);
await LoadAdminPlants(
AdminPlantEditingKey
);
SetAdminPlantMessage(
"Stage " +
Stage +
" sprite removed."
);
} catch (Error) {
console.error(
"Couldn't remove plant sprite:",
Error
);
SetAdminPlantMessage(
Error.message ??
"Couldn't remove plant sprite."
);
} finally {
AdminPlantImagePending = false;
RenderAdminPlantImageManager();
}
} }
+1 -1
View File
@@ -1,7 +1,7 @@
const GameContentCacheName = const GameContentCacheName =
"SarahtoninGardenContent"; "SarahtoninGardenContent";
const GameContentCacheSchemaVersion = 2; const GameContentCacheSchemaVersion = 3;
/* /*
* Once content has been checked, page loads for the next * Once content has been checked, page loads for the next
+327 -1
View File
@@ -269,6 +269,14 @@ function GetPlantShopCost(
Plant.Shop?.ShopPlant === Plant.Shop?.ShopPlant ===
true true
) { ) {
if (
Plant.Shop.BaseCost === null ||
Plant.Shop.BaseCost === undefined ||
Plant.Shop.BaseCost === ""
) {
return null;
}
const BaseCost = Number( const BaseCost = Number(
Plant.Shop.BaseCost Plant.Shop.BaseCost
); );
@@ -599,6 +607,35 @@ function GetMutationTimeFee(
} }
function GetPlantHarvestMultiplier(
PlantId
) {
const Plant =
typeof PlantId === "object" &&
PlantId !== null
? PlantId
: GetPlantById(
PlantId
);
if (Plant === null) {
return 1.5;
}
const Multiplier = Number(
Plant.HarvestMultiplier ??
1.5
);
return (
Number.isFinite(Multiplier) &&
Multiplier >= 0
)
? Multiplier
: 1.5;
}
function GetPlantHarvestReward( function GetPlantHarvestReward(
SaveData, SaveData,
PlantId PlantId
@@ -614,7 +651,10 @@ function GetPlantHarvestReward(
} }
return Math.ceil( return Math.ceil(
Cost * 1.5 Cost *
GetPlantHarvestMultiplier(
PlantId
)
); );
} }
@@ -699,3 +739,289 @@ function FormatDewPerHour(
" Dew" " Dew"
); );
} }
function GetCataloguePlantById(
PlantId,
PlantCatalogue = Plants
) {
PlantId = Number(PlantId);
for (
const Plant
of Object.values(
PlantCatalogue
)
) {
if (Number(Plant.Id) === PlantId) {
return Plant;
}
}
return null;
}
function GetCataloguePlantCostInfo(
PlantId,
PlantCatalogue = Plants,
MutationCatalogue = MutationSets,
Cache = new Map(),
Visiting = new Set()
) {
PlantId = Number(PlantId);
if (Cache.has(PlantId)) {
return Cache.get(PlantId);
}
const Plant =
GetCataloguePlantById(
PlantId,
PlantCatalogue
);
if (Plant === null) {
return null;
}
if (Plant.Shop?.ShopPlant === true) {
if (
Plant.Shop.BaseCost === null ||
Plant.Shop.BaseCost === undefined ||
Plant.Shop.BaseCost === ""
) {
return null;
}
const BaseCost = Number(
Plant.Shop.BaseCost
);
if (
!Number.isFinite(BaseCost) ||
BaseCost < 0
) {
return null;
}
const Info = {
Cost: Math.ceil(BaseCost),
SourceType: "Base",
SourceMutation: null
};
Cache.set(PlantId, Info);
return Info;
}
if (Visiting.has(PlantId)) {
return null;
}
Visiting.add(PlantId);
let CheapestInfo = null;
for (
const [MutationKey, Mutation]
of Object.entries(
MutationCatalogue
)
) {
if (Mutation.Archived === true) {
continue;
}
if (
!Mutation.Relations
?.PlantsCreated
?.includes(PlantId)
) {
continue;
}
const IngredientCost =
GetCatalogueMutationIngredientCost(
Mutation,
PlantCatalogue,
MutationCatalogue,
Cache,
Visiting
);
if (IngredientCost === null) {
continue;
}
const Cost =
IngredientCost +
GetMutationTimeFee(
Mutation
);
if (
CheapestInfo === null ||
Cost < CheapestInfo.Cost
) {
CheapestInfo = {
Cost: Cost,
SourceType: "Mutation",
SourceMutation: {
Id: Number(
Mutation.Id
),
MutationKey: MutationKey,
Name:
Mutation.Name ??
MutationKey
}
};
}
}
Visiting.delete(PlantId);
Cache.set(PlantId, CheapestInfo);
return CheapestInfo;
}
function GetCatalogueMutationIngredientCost(
Mutation,
PlantCatalogue,
MutationCatalogue,
Cache,
Visiting
) {
let TotalCost = 0;
for (const Row of Mutation.Pattern ?? []) {
for (const Matcher of Row) {
if (
Matcher === null ||
Matcher === "Empty" ||
Matcher === "Any"
) {
continue;
}
const MatcherCost =
GetCatalogueMatcherSeedCost(
Matcher,
PlantCatalogue,
MutationCatalogue,
Cache,
Visiting
);
if (MatcherCost === null) {
return null;
}
TotalCost += MatcherCost;
}
}
return TotalCost;
}
function GetCatalogueMatcherSeedCost(
Matcher,
PlantCatalogue,
MutationCatalogue,
Cache,
Visiting
) {
if (typeof Matcher === "string") {
const Plant =
PlantCatalogue[Matcher];
if (
Plant === undefined ||
Plant.Archived === true
) {
return null;
}
return GetCataloguePlantCostInfo(
Plant.Id,
PlantCatalogue,
MutationCatalogue,
Cache,
Visiting
)?.Cost ?? null;
}
if (
typeof Matcher !== "object" ||
Matcher === null
) {
return null;
}
if (typeof Matcher.Plant === "string") {
const Plant =
PlantCatalogue[Matcher.Plant];
if (
Plant === undefined ||
Plant.Archived === true
) {
return null;
}
return GetCataloguePlantCostInfo(
Plant.Id,
PlantCatalogue,
MutationCatalogue,
Cache,
Visiting
)?.Cost ?? null;
}
let CheapestCost = null;
for (
const Plant
of Object.values(
PlantCatalogue
)
) {
if (Plant.Archived === true) {
continue;
}
if (
!DoesPlantMatchRecipeMatcher(
Plant,
Matcher
)
) {
continue;
}
const Cost =
GetCataloguePlantCostInfo(
Plant.Id,
PlantCatalogue,
MutationCatalogue,
Cache,
Visiting
)?.Cost ?? null;
if (Cost === null) {
continue;
}
if (
CheapestCost === null ||
Cost < CheapestCost
) {
CheapestCost = Cost;
}
}
return CheapestCost;
}
+3 -3
View File
@@ -1303,9 +1303,9 @@ function GetPlantImages(
Plot, Plot,
Plant Plant
) { ) {
return PlantImages[ return GetPlantImageSources(
Plot.Plant Plant ?? Plot.Plant
] ?? []; );
} }
+8 -6
View File
@@ -28,13 +28,17 @@ function RenderGuideRecipePlants() {
const Plant = const Plant =
Plants[PlantKey]; Plants[PlantKey];
const Images = const MatureImage =
PlantImages[PlantKey] ?? []; Plant === undefined
? null
: GetPlantMatureImageSource(
Plant
);
if ( if (
Plant === undefined || Plant === undefined ||
Images.length === 0 MatureImage === null
) { ) {
continue; continue;
} }
@@ -49,9 +53,7 @@ function RenderGuideRecipePlants() {
"PlantSprite GuideRecipeImage"; "PlantSprite GuideRecipeImage";
Image.src = Image.src =
Images[ MatureImage;
Images.length - 1
];
Image.alt = ""; Image.alt = "";
+6 -6
View File
@@ -1051,11 +1051,13 @@ function CreateMutationPlantCell(
"PlantTile GuideRecipeCell GuideRecipePlant"; "PlantTile GuideRecipeCell GuideRecipePlant";
const Images = const MatureImage =
PlantImages[PlantKey] ?? []; GetPlantMatureImageSource(
Plant
);
if (Images.length > 0) { if (MatureImage !== null) {
const Image = const Image =
document.createElement( document.createElement(
"img" "img"
@@ -1065,9 +1067,7 @@ function CreateMutationPlantCell(
"PlantSprite GuideRecipeImage"; "PlantSprite GuideRecipeImage";
Image.src = Image.src =
Images[ MatureImage;
Images.length - 1
];
Image.alt = Image.alt =
""; "";
+5 -14
View File
@@ -178,20 +178,13 @@ function CreatePlantEncyclopediaVisual(
"PlantTile PlantEncyclopediaVisual"; "PlantTile PlantEncyclopediaVisual";
const PlantKey = const MatureImage =
GetPlantKeyById( GetPlantMatureImageSource(
Plant.Id Plant
); );
const Images =
PlantKey === null
? []
: PlantImages[
PlantKey
] ?? [];
if (MatureImage === null) {
if (Images.length === 0) {
const Missing = const Missing =
document.createElement( document.createElement(
"span" "span"
@@ -220,9 +213,7 @@ function CreatePlantEncyclopediaVisual(
"PlantSprite"; "PlantSprite";
Image.src = Image.src =
Images[ MatureImage;
Images.length - 1
];
Image.alt = Image.alt =
Plant.Name; Plant.Name;
+137
View File
@@ -48,3 +48,140 @@ async function FetchPlantsFromApi() {
return Result.Plants; return Result.Plants;
} }
function GetPlantImageSources(
PlantOrKey
) {
const PlantKey =
typeof PlantOrKey === "string"
? PlantOrKey
: Object.entries(
Plants
).find(
([, Candidate]) =>
Candidate ===
PlantOrKey ||
Number(Candidate?.Id) ===
Number(PlantOrKey?.Id)
)?.[0] ?? null;
const Plant =
typeof PlantOrKey === "string"
? Plants[PlantOrKey]
: PlantOrKey;
const StaticImages =
PlantKey !== null &&
typeof PlantKey === "string" &&
Array.isArray(
PlantImages[PlantKey]
)
? PlantImages[PlantKey]
: [];
const ApiStages =
Array.isArray(
Plant?.ImageStages
)
? Plant.ImageStages
: [];
let HighestStage =
StaticImages.length;
for (const ImageStage of ApiStages) {
const Stage = Number(
typeof ImageStage === "object" &&
ImageStage !== null
? ImageStage.Stage
: ImageStage
);
if (
Number.isInteger(Stage) &&
Stage > HighestStage
) {
HighestStage = Stage;
}
}
const Images =
Array.from(
{length: HighestStage},
(_, Index) =>
StaticImages[Index] ?? null
);
if (Plant === undefined || Plant === null) {
return Images;
}
for (const ImageStage of ApiStages) {
const Stage = Number(
typeof ImageStage === "object" &&
ImageStage !== null
? ImageStage.Stage
: ImageStage
);
if (
!Number.isInteger(Stage) ||
Stage < 1
) {
continue;
}
const Revision =
typeof ImageStage === "object" &&
ImageStage !== null
? Number(
ImageStage.Revision ?? 0
)
: 0;
const Query =
new URLSearchParams({
PlantId: String(Plant.Id),
Stage: String(Stage),
v: String(
Number.isFinite(Revision)
? Revision
: 0
)
});
Images[Stage - 1] =
ApiUrl +
"/PlantImage.php?" +
Query.toString();
}
return Images;
}
function GetPlantMatureImageSource(
PlantOrKey
) {
const Images =
GetPlantImageSources(
PlantOrKey
);
for (
let Index = Images.length - 1;
Index >= 0;
Index--
) {
if (
typeof Images[Index] ===
"string" &&
Images[Index].length > 0
) {
return Images[Index];
}
}
return null;
}
+5 -14
View File
@@ -951,20 +951,13 @@ function CreateShopPlantVisual(
"PlantTile ShopPlantImageFrame"; "PlantTile ShopPlantImageFrame";
const PlantKey = const MatureImage =
GetPlantKeyById( GetPlantMatureImageSource(
Plant.Id Plant
); );
const Images =
PlantKey === null
? []
: PlantImages[
PlantKey
] ?? [];
if (MatureImage === null) {
if (Images.length === 0) {
const Missing = const Missing =
document.createElement( document.createElement(
"span" "span"
@@ -989,9 +982,7 @@ function CreateShopPlantVisual(
"PlantSprite"; "PlantSprite";
Image.src = Image.src =
Images[ MatureImage;
Images.length - 1
];
Image.alt = Image.alt =
Plant.Name; Plant.Name;
+126
View File
@@ -1884,3 +1884,129 @@ input, button, textarea, select {
width: 100%; width: 100%;
} }
} }
.AdminDurationInputs {
display: grid;
grid-template-columns: repeat( 3, minmax(0, 1fr) );
gap: 0.5rem;
}
.AdminDurationInputs label {
display: flex;
flex-direction: column;
gap: 0.25rem;
min-width: 0;
}
.AdminDurationInputs label > span {
color: var(--Overlay2);
font-size: 0.9rem;
}
.AdminPlantEconomyPreview {
margin-top: 1rem;
}
.AdminPlantEconomyPreview h3 {
margin-bottom: 0.5rem;
color: var(--Peach);
}
.AdminPlantEconomyPreview dl {
display: grid;
grid-template-columns: repeat( 3, minmax(0, 1fr) );
gap: 0.65rem 1rem;
margin: 0;
}
.AdminPlantEconomyPreview dl > div {
min-width: 0;
}
.AdminPlantEconomyPreview dt {
color: var(--Overlay2);
}
.AdminPlantEconomyPreview dd {
margin: 0.15rem 0 0;
overflow-wrap: anywhere;
}
.AdminPlantEconomySource {
grid-column: span 2;
}
.AdminPlantImagesSection {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 3px solid var(--Surface2);
}
.AdminPlantImagesSection h2 {
margin-bottom: 0.5rem;
font-size: 1.4rem;
}
.AdminPlantImageUpload {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
margin-top: 0.75rem;
}
.AdminPlantImageUpload input[type="file"] {
flex: 1 1 18rem;
}
.AdminPlantImageNote {
color: var(--Overlay2);
}
.AdminPlantImageList {
display: grid;
gap: 0.65rem;
margin-top: 0.75rem;
}
.AdminPlantImageRow {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
padding: 0.65rem;
border: 3px solid var(--Surface1);
}
.AdminPlantImagePreview {
display: flex;
width: 4rem;
height: 4rem;
align-items: center;
justify-content: center;
}
.AdminPlantImageRemoveButton {
--ActionAccent: var(--Red);
width: auto;
}
@media (max-width: 700px) {
.AdminDurationInputs,
.AdminPlantEconomyPreview dl {
grid-template-columns: 1fr;
}
.AdminPlantEconomySource {
grid-column: auto;
}
.AdminPlantImageRow {
grid-template-columns: auto minmax(0, 1fr);
}
.AdminPlantImageRemoveButton {
grid-column: 1 / -1;
width: 100%;
}
}