Added editor pages for plants and mutations.

This commit is contained in:
sarah committed 2026-09-01 20:23:16 +02:00
1 parent 953df07878
commit c2a78b18b4
6 files changed
+4288 -2

No files matched your search

+24 -2
View File
@@ -30,9 +30,31 @@ $PageSection = "Admin";
hidden hidden
> >
<p> <p>
Admin access verified. Garden content Content tools for editing the Garden catalogue.
editors will live here. Changes here affect the live game data.
</p> </p>
<div class="AdminEditorNavigation">
<a
class="ActionButton AdminEditorLink"
href="/Pages/AdminPlants.html"
>
<strong>Plant editor</strong>
<span>
Add plants and edit their catalogue data.
</span>
</a>
<a
class="ActionButton AdminEditorLink"
href="/Pages/AdminMutations.html"
>
<strong>Mutation editor</strong>
<span>
Build mutation recipes with a visual grid.
</span>
</a>
</div>
</div> </div>
</main> </main>
</body> </body>
+432
View File
@@ -0,0 +1,432 @@
<?php
$PageTitle = "Admin mutations";
$PageSection = "Admin";
?>
<!DOCTYPE html>
<html lang="en" data-palette="CatppuccinMocha">
<?php require __DIR__ . "/Bits/Head.php"; ?>
<body>
<?php require __DIR__ . "/Bits/Navbar.php"; ?>
<main class="MainContainer AdminMutationMain">
<h1>Mutation editor</h1>
<p>
Edit mutation settings and build the recipe visually.
Select a cell in either grid to change what it means.
</p>
<p
id="AdminAccessMessage"
class="PageMessage"
aria-live="polite"
>
Checking admin access...
</p>
<div
id="AdminContent"
hidden
>
<div class="AdminEditorToolbar">
<label for="AdminMutationSelect">
Mutation
</label>
<select id="AdminMutationSelect"></select>
<button
id="AdminMutationNewButton"
class="ActionButton AdminInlineButton"
type="button"
>
New mutation
</button>
</div>
<form
id="AdminMutationForm"
class="Panel AdminEditorForm"
>
<div class="AdminEditorGrid">
<label class="AdminEditorField">
<span>ID</span>
<input
id="AdminMutationId"
type="number"
min="1"
step="1"
required
>
<small>
Permanent. New mutations default to the next ID.
</small>
</label>
<label class="AdminEditorField">
<span>Mutation Key</span>
<input
id="AdminMutationKey"
type="text"
maxlength="64"
required
>
</label>
<label class="AdminEditorField AdminEditorWideField">
<span>Name</span>
<input
id="AdminMutationName"
type="text"
maxlength="128"
required
>
</label>
<label class="AdminEditorField AdminEditorWideField">
<span>Description</span>
<textarea
id="AdminMutationDescription"
rows="4"
required
></textarea>
</label>
<label class="AdminEditorField">
<span>Priority</span>
<input
id="AdminMutationPriority"
type="number"
step="1"
required
>
</label>
<label class="AdminEditorField">
<span>Chance (%)</span>
<input
id="AdminMutationChance"
type="number"
min="0"
max="100"
step="0.01"
required
>
</label>
<label class="AdminEditorField">
<span>Cooldown (ms)</span>
<input
id="AdminMutationCooldown"
type="number"
min="0"
step="1000"
required
>
<small id="AdminMutationCooldownHint"></small>
</label>
<label class="AdminEditorField">
<span>Rotation</span>
<select id="AdminMutationRotation">
<option value="None">None</option>
<option value="Any">Any</option>
</select>
</label>
<label class="AdminEditorCheckField">
<input
id="AdminMutationAllowImmature"
type="checkbox"
>
<span>
Allow immature plants
</span>
</label>
</div>
<section class="AdminMutationRecipeSection">
<div class="AdminMutationRecipeHeader">
<div>
<h2>Visual recipe</h2>
<p>
Pattern and result always use the same dimensions.
</p>
</div>
<div class="AdminMutationDimensions">
<label>
Width
<input
id="AdminMutationWidth"
type="number"
min="1"
max="7"
step="1"
value="3"
>
</label>
<label>
Height
<input
id="AdminMutationHeight"
type="number"
min="1"
max="7"
step="1"
value="1"
>
</label>
<button
id="AdminMutationResizeButton"
class="ActionButton AdminInlineButton"
type="button"
>
Resize
</button>
</div>
</div>
<div class="AdminMutationRecipeFlow">
<div class="AdminMutationGridPanel">
<h3>Arrange</h3>
<div
id="AdminMutationPatternGrid"
class="GuideRecipe AdminMutationGrid"
></div>
</div>
<span
class="MutationEncyclopediaRecipeArrow"
aria-hidden="true"
>
→
</span>
<div class="AdminMutationGridPanel">
<h3>Result</h3>
<div
id="AdminMutationResultGrid"
class="GuideRecipe AdminMutationGrid"
></div>
</div>
</div>
<div
id="AdminMutationCellEditor"
class="AdminMutationCellEditor"
hidden
>
<h3 id="AdminMutationCellEditorTitle">
Cell
</h3>
<div id="AdminMutationPatternCellControls">
<label class="AdminEditorField">
<span>Type</span>
<select id="AdminMutationPatternCellType">
<option value="Any">Any</option>
<option value="Empty">Empty</option>
<option value="Plant">Exact plant</option>
<option value="Matcher">Tag matcher</option>
</select>
</label>
<label
id="AdminMutationPatternPlantRow"
class="AdminEditorField"
hidden
>
<span>Plant</span>
<select id="AdminMutationPatternPlant"></select>
</label>
<div
id="AdminMutationMatcherRows"
class="AdminMatcherFields"
hidden
>
<label class="AdminEditorField">
<span>Exact plant (optional)</span>
<select id="AdminMutationMatcherPlant"></select>
</label>
<label class="AdminEditorField">
<span>Required tags</span>
<input
id="AdminMutationMatcherTags"
type="text"
placeholder="Rose, Red"
>
</label>
<label class="AdminEditorField">
<span>Any of these tags</span>
<input
id="AdminMutationMatcherTagsAny"
type="text"
>
</label>
<label class="AdminEditorField">
<span>Excluded tags</span>
<input
id="AdminMutationMatcherTagsNot"
type="text"
>
</label>
<label class="AdminEditorField">
<span>Capture name</span>
<input
id="AdminMutationMatcherCapture"
type="text"
placeholder="ParentA"
>
</label>
</div>
</div>
<div
id="AdminMutationResultCellControls"
hidden
>
<label class="AdminEditorField">
<span>Type</span>
<select id="AdminMutationResultCellType">
<option value="Keep">Keep</option>
<option value="Empty">Empty</option>
<option value="Plant">Plant</option>
<option value="Capture">Captured plant</option>
</select>
</label>
<label
id="AdminMutationResultPlantRow"
class="AdminEditorField"
hidden
>
<span>Plant</span>
<select id="AdminMutationResultPlant"></select>
</label>
<label
id="AdminMutationResultCaptureRow"
class="AdminEditorField"
hidden
>
<span>Capture name</span>
<input
id="AdminMutationResultCapture"
type="text"
placeholder="ParentA"
>
</label>
</div>
</div>
</section>
<section class="AdminMutationRelationsSection">
<h2>Plant relationships</h2>
<p>
These power discoveries and encyclopedia links.
They are metadata, so tag-based recipes may still
need explicit plants selected here.
</p>
<div class="AdminMutationRelationColumns">
<fieldset>
<legend>Plants used</legend>
<div
id="AdminMutationPlantsUsed"
class="AdminPlantChecklist"
></div>
</fieldset>
<fieldset>
<legend>Plants created</legend>
<div
id="AdminMutationPlantsCreated"
class="AdminPlantChecklist"
></div>
</fieldset>
</div>
</section>
<section class="AdminMutationFailureSection">
<h2>Failure result</h2>
<label class="AdminEditorField">
<span>On failure</span>
<select id="AdminMutationFailureType">
<option value="Keep">Keep everything</option>
<option value="Clear">Clear matched area</option>
<option value="Custom">Custom result JSON</option>
</select>
</label>
<label
id="AdminMutationFailureJsonRow"
class="AdminEditorField"
hidden
>
<span>Custom failure result JSON</span>
<textarea
id="AdminMutationFailureJson"
rows="6"
spellcheck="false"
></textarea>
</label>
</section>
<details class="AdminGeneratedJson">
<summary>Generated mutation JSON</summary>
<pre id="AdminMutationJsonPreview"></pre>
</details>
<p id="AdminMutationArchiveNote" hidden>
This mutation is currently archived.
</p>
<div class="AdminEditorActions">
<button
id="AdminMutationSaveButton"
class="ActionButton AdminSaveButton"
type="submit"
>
Save mutation
</button>
<a
class="ActionButton AdminInlineButton AdminEditorBackLink"
href="/Pages/Admin.html"
>
Back to Admin
</a>
</div>
</form>
<p
id="AdminMutationMessage"
class="PageMessage"
aria-live="polite"
></p>
</div>
</main>
<script src="/Scripts/Garden/PlantImages.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/AdminMutationEditor.js"></script>
</body>
</html>
+225
View File
@@ -0,0 +1,225 @@
<?php
$PageTitle = "Admin plants";
$PageSection = "Admin";
?>
<!DOCTYPE html>
<html lang="en" data-palette="CatppuccinMocha">
<?php require __DIR__ . "/Bits/Head.php"; ?>
<body>
<?php require __DIR__ . "/Bits/Navbar.php"; ?>
<main class="MainContainer">
<h1>Plant editor</h1>
<p>
Add plants or edit existing catalogue data. Numeric IDs
and existing Plant Keys stay permanent once created.
</p>
<p
id="AdminAccessMessage"
class="PageMessage"
aria-live="polite"
>
Checking admin access...
</p>
<div
id="AdminContent"
hidden
>
<div class="AdminEditorToolbar">
<label for="AdminPlantSelect">
Plant
</label>
<select id="AdminPlantSelect"></select>
<button
id="AdminPlantNewButton"
class="ActionButton AdminInlineButton"
type="button"
>
New plant
</button>
</div>
<form
id="AdminPlantForm"
class="Panel AdminEditorForm"
>
<div class="AdminEditorGrid">
<label class="AdminEditorField">
<span>ID</span>
<input
id="AdminPlantId"
name="Id"
type="number"
min="1"
step="1"
required
>
<small>
Permanent. New plants default to the next ID.
</small>
</label>
<label class="AdminEditorField">
<span>Plant Key</span>
<input
id="AdminPlantKey"
name="PlantKey"
type="text"
maxlength="64"
required
>
<small>
Used by saves, recipes and the sprite folder.
</small>
</label>
<label class="AdminEditorField AdminEditorWideField">
<span>Name</span>
<input
id="AdminPlantName"
name="Name"
type="text"
maxlength="64"
required
>
</label>
<label class="AdminEditorField AdminEditorWideField">
<span>Description</span>
<textarea
id="AdminPlantDescription"
name="Description"
rows="4"
required
></textarea>
</label>
<label class="AdminEditorField AdminEditorWideField">
<span>Tags</span>
<input
id="AdminPlantTags"
name="Tags"
type="text"
placeholder="Rose, Flower, Red"
>
<small>
Comma-separated. Spaces around tags are ignored.
</small>
</label>
<label class="AdminEditorField">
<span>Growth time (ms)</span>
<input
id="AdminPlantGrowthTime"
name="GrowthTime"
type="number"
min="0"
step="1000"
required
>
<small id="AdminPlantGrowthTimeHint"></small>
</label>
<label class="AdminEditorField">
<span>Base seed cost</span>
<input
id="AdminPlantBaseCost"
name="BaseCost"
type="number"
min="0"
step="1"
>
<small>
Used only for plants stocked from the start.
</small>
</label>
<label class="AdminEditorCheckField">
<input
id="AdminPlantShopPlant"
name="ShopPlant"
type="checkbox"
>
<span>
Available in the Shop from the beginning
</span>
</label>
<label class="AdminEditorField AdminEditorWideField">
<span>Effects JSON</span>
<textarea
id="AdminPlantEffects"
name="Effects"
rows="6"
spellcheck="false"
></textarea>
<small>
Keep this as <code>{}</code> when the plant has no effects.
</small>
</label>
</div>
<div class="AdminEditorPreview">
<h2>Preview</h2>
<div class="AdminPlantPreviewRow">
<div
id="AdminPlantPreviewTile"
class="PlantTile AdminPlantPreviewTile"
></div>
<div>
<strong id="AdminPlantPreviewName">
New plant
</strong>
<p id="AdminPlantPreviewKey"></p>
<p id="AdminPlantArchiveNote" hidden>
This plant is currently archived.
</p>
</div>
</div>
</div>
<div class="AdminEditorActions">
<button
id="AdminPlantSaveButton"
class="ActionButton AdminSaveButton"
type="submit"
>
Save plant
</button>
<a
class="ActionButton AdminInlineButton AdminEditorBackLink"
href="/Pages/Admin.html"
>
Back to Admin
</a>
</div>
</form>
<p
id="AdminPlantMessage"
class="PageMessage"
aria-live="polite"
></p>
</div>
</main>
<script src="/Scripts/Garden/PlantImages.js"></script>
<script src="/Scripts/Garden/Plants.js"></script>
<script src="/Scripts/Garden/Save.js"></script>
<script src="/Scripts/Garden/AdminPlantEditor.js"></script>
</body>
</html>
File diff suppressed because it is too large. Load diff
+863
View File
@@ -0,0 +1,863 @@
let AdminPlantCatalogue = {};
let AdminPlantEditingKey = null;
let AdminPlantSavePending = 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(
"AdminPlantForm"
).addEventListener(
"submit",
SaveAdminPlant
);
for (
const ElementId
of [
"AdminPlantId",
"AdminPlantKey",
"AdminPlantName",
"AdminPlantGrowthTime",
"AdminPlantShopPlant",
"AdminPlantBaseCost"
]
) {
document.getElementById(
ElementId
).addEventListener(
"input",
RenderAdminPlantPreview
);
document.getElementById(
ElementId
).addEventListener(
"change",
RenderAdminPlantPreview
);
}
document.getElementById(
"AdminPlantShopPlant"
).addEventListener(
"change",
UpdateAdminPlantShopFields
);
}
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 LoadAdminPlants(
PreferredPlantKey = null
) {
SetAdminPlantMessage(
"Loading plants..."
);
const Result =
await AdminPlantRequest(
"List"
);
AdminPlantCatalogue =
Result.Plants ?? {};
Plants =
AdminPlantCatalogue;
RenderAdminPlantSelect();
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 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(", ")
: ""
);
SetAdminPlantField(
"AdminPlantGrowthTime",
Number(
Plant.GrowthTime ?? 0
)
);
document.getElementById(
"AdminPlantShopPlant"
).checked =
Plant.Shop?.ShopPlant === 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 StartNewAdminPlant() {
AdminPlantEditingKey = null;
document.getElementById(
"AdminPlantSelect"
).value = "";
SetAdminPlantField(
"AdminPlantId",
GetNextAdminPlantId()
);
SetAdminPlantField(
"AdminPlantKey",
""
);
SetAdminPlantField(
"AdminPlantName",
""
);
SetAdminPlantField(
"AdminPlantDescription",
""
);
SetAdminPlantField(
"AdminPlantTags",
""
);
SetAdminPlantField(
"AdminPlantGrowthTime",
3600000
);
document.getElementById(
"AdminPlantShopPlant"
).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
),
Effects:
ParseAdminPlantEffects(),
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;
}
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 Images =
PlantImages[
PlantKey
] ?? [];
if (
Array.isArray(Images) &&
Images.length > 0
) {
const Image =
document.createElement(
"img"
);
Image.className =
"PlantSprite";
Image.src =
Images[
Images.length - 1
];
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)
? FormatAdminDuration(
GrowthTime
)
: "";
}
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
);
+478
View File
@@ -1722,3 +1722,481 @@ select {
text-align: left; text-align: left;
} }
} }
/*
* Admin editors
*/
.AdminEditorNavigation {
display: grid;
grid-template-columns:
repeat(
2,
minmax(0, 1fr)
);
gap: 1rem;
margin-top: 1.5rem;
}
.AdminEditorLink {
display: flex;
flex-direction: column;
gap: 0.35rem;
min-height: 7rem;
align-items: flex-start;
justify-content: center;
padding: 1rem;
text-decoration: none;
}
.AdminEditorLink strong {
color: var(--Green);
font-size: 1.2rem;
}
.AdminEditorLink span {
color: var(--Subtext1);
line-height: 1.35;
}
.AdminEditorToolbar {
display: grid;
grid-template-columns:
auto
minmax(0, 1fr)
auto;
gap: 0.75rem;
align-items: center;
margin: 1.5rem 0 1rem;
}
.AdminEditorToolbar label {
color: var(--Blue);
}
.AdminEditorForm {
padding: 1rem;
}
.AdminEditorGrid {
display: grid;
grid-template-columns:
repeat(
2,
minmax(0, 1fr)
);
gap: 1rem;
}
.AdminEditorField {
display: flex;
flex-direction: column;
gap: 0.4rem;
min-width: 0;
}
.AdminEditorField > span,
.AdminMutationDimensions label {
color: var(--Blue);
}
.AdminEditorField small {
color: var(--Overlay2);
line-height: 1.25;
}
.AdminEditorWideField {
grid-column: 1 / -1;
}
.AdminEditorForm input,
.AdminEditorForm textarea,
.AdminEditorForm select,
.AdminEditorToolbar select,
.AdminMutationDimensions input {
width: 100%;
min-width: 0;
padding: 0.55rem;
color: var(--Text);
background-color: var(--Surface0);
border: 3px solid var(--Surface2);
border-radius: 0;
outline: none;
}
.AdminEditorForm textarea {
resize: vertical;
}
.AdminEditorForm input:focus,
.AdminEditorForm textarea:focus,
.AdminEditorForm select:focus,
.AdminEditorToolbar select:focus,
.AdminMutationDimensions input:focus {
border-color: var(--Sky);
}
.AdminEditorForm input:read-only {
color: var(--Subtext0);
background-color: var(--Base);
}
.AdminEditorForm input:disabled {
opacity: 0.55;
}
.AdminEditorCheckField {
display: flex;
gap: 0.65rem;
align-items: center;
align-self: end;
min-height: 2.8rem;
}
.AdminEditorCheckField input {
width: 1.2rem;
height: 1.2rem;
margin: 0;
accent-color: var(--Green);
}
.AdminInlineButton {
width: auto;
min-width: max-content;
}
.AdminSaveButton {
--ActionAccent: var(--Green);
width: auto;
}
.AdminEditorBackLink {
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
}
.AdminEditorActions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 1.25rem;
}
.AdminEditorPreview,
.AdminMutationRecipeSection,
.AdminMutationRelationsSection,
.AdminMutationFailureSection {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 3px solid var(--Surface2);
}
.AdminEditorPreview h2,
.AdminMutationRecipeSection h2,
.AdminMutationRelationsSection h2,
.AdminMutationFailureSection h2 {
margin-bottom: 0.5rem;
font-size: 1.4rem;
}
.AdminPlantPreviewRow {
display: flex;
gap: 1rem;
align-items: center;
}
.AdminPlantPreviewTile {
display: flex;
width: 7rem;
height: 7rem;
flex: 0 0 auto;
align-items: center;
justify-content: center;
}
.AdminPlantPreviewMissing {
padding: 0.5rem;
color: var(--Overlay1);
text-align: center;
}
#AdminPlantPreviewKey,
#AdminPlantArchiveNote,
#AdminMutationArchiveNote {
margin: 0.35rem 0 0;
color: var(--Overlay2);
}
#AdminPlantArchiveNote,
#AdminMutationArchiveNote {
color: var(--Yellow);
}
.AdminMutationRecipeHeader {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: end;
}
.AdminMutationRecipeHeader p,
.AdminMutationRelationsSection > p {
margin-bottom: 0;
color: var(--Subtext1);
}
.AdminMutationDimensions {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
align-items: end;
}
.AdminMutationDimensions label {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.AdminMutationDimensions input {
width: 5rem;
}
.AdminMutationRecipeFlow {
display: grid;
grid-template-columns:
minmax(0, 1fr)
auto
minmax(0, 1fr);
gap: 0.75rem;
align-items: center;
margin-top: 1rem;
}
.AdminMutationGridPanel {
min-width: 0;
}
.AdminMutationGridPanel h3 {
margin-bottom: 0.25rem;
color: var(--Subtext1);
font-size: 1rem;
text-align: center;
}
.AdminMutationGrid {
width: 100%;
margin: 0.5rem auto;
}
.AdminMutationGridCell {
width: 100%;
cursor: pointer;
}
.AdminMutationGridCell:hover {
border-color: var(--Sky);
}
.AdminMutationGridCellSelected {
border-color: var(--Yellow) !important;
box-shadow:
0 0 0 2px
var(--Crust);
}
.AdminMutationCellEditor {
margin-top: 1rem;
padding: 1rem;
background-color: var(--Base);
border: 3px solid var(--Yellow);
}
.AdminMutationCellEditor h3 {
margin-bottom: 0.75rem;
color: var(--Yellow);
}
#AdminMutationPatternCellControls,
#AdminMutationResultCellControls,
.AdminMatcherFields {
display: grid;
grid-template-columns:
repeat(
2,
minmax(0, 1fr)
);
gap: 0.75rem;
}
.AdminMatcherFields {
grid-column: 1 / -1;
}
.AdminMutationRelationColumns {
display: grid;
grid-template-columns:
repeat(
2,
minmax(0, 1fr)
);
gap: 1rem;
margin-top: 1rem;
}
.AdminMutationRelationColumns fieldset {
min-width: 0;
margin: 0;
padding: 0.75rem;
background-color: var(--Base);
border: 3px solid var(--Surface2);
}
.AdminMutationRelationColumns legend {
padding: 0 0.35rem;
color: var(--Peach);
}
.AdminPlantChecklist {
display: flex;
flex-direction: column;
gap: 0.45rem;
max-height: 14rem;
overflow-y: auto;
}
.AdminPlantChecklistItem {
display: flex;
gap: 0.55rem;
align-items: center;
}
.AdminPlantChecklistItem input {
width: 1rem;
height: 1rem;
margin: 0;
accent-color: var(--Green);
}
.AdminGeneratedJson {
margin-top: 1.5rem;
padding: 0.75rem;
background-color: var(--Base);
border: 3px solid var(--Surface2);
}
.AdminGeneratedJson summary {
cursor: pointer;
color: var(--Sky);
}
.AdminGeneratedJson pre {
max-height: 24rem;
margin: 0.75rem 0 0;
padding: 0.75rem;
overflow: auto;
color: var(--Subtext1);
background-color: var(--Crust);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
@media (max-width: 700px) {
.AdminEditorNavigation,
.AdminEditorGrid,
#AdminMutationPatternCellControls,
#AdminMutationResultCellControls,
.AdminMatcherFields,
.AdminMutationRelationColumns {
grid-template-columns: 1fr;
}
.AdminEditorToolbar {
grid-template-columns: 1fr;
}
.AdminEditorToolbar label {
margin-bottom: -0.35rem;
}
.AdminInlineButton,
.AdminSaveButton {
width: 100%;
}
.AdminMutationRecipeHeader {
flex-direction: column;
align-items: stretch;
}
.AdminMutationRecipeFlow {
grid-template-columns: 1fr;
}
.AdminMutationRecipeFlow >
.MutationEncyclopediaRecipeArrow {
justify-self: center;
transform: rotate(90deg);
}
.AdminMutationDimensions {
align-items: stretch;
}
.AdminMutationDimensions label {
flex: 1 1 5rem;
}
.AdminMutationDimensions input {
width: 100%;
}
}