mirror of
https://git.srhtnin.garden/sarah/srhtnin.github.io.git
synced 2026-10-14 18:58:11 +02:00
Added more Admin QOL stuff. DW.
This commit is contained in:
1 parent
ab0d858fef
commit
24842c65fb
12 files changed
+4236
No files matched your search
@@ -54,8 +54,61 @@ $PageSection = "Admin";
|
|||||||
Build mutation recipes with a visual grid.
|
Build mutation recipes with a visual grid.
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="ActionButton AdminEditorLink"
|
||||||
|
href="/Pages/AdminMassPlants.html"
|
||||||
|
>
|
||||||
|
<strong>Mass plant editor</strong>
|
||||||
|
<span>
|
||||||
|
Apply selected plant fields to several plants at once.
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="ActionButton AdminEditorLink"
|
||||||
|
href="/Pages/AdminMassMutations.html"
|
||||||
|
>
|
||||||
|
<strong>Mass mutation editor</strong>
|
||||||
|
<span>
|
||||||
|
Apply selected mutation fields to several mutations at once.
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="AdminOverview"
|
||||||
|
class="Panel AdminOverview"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<div class="PanelHeader">
|
||||||
|
<h2>Quick overview</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p
|
||||||
|
id="AdminOverviewMessage"
|
||||||
|
class="PageMessage"
|
||||||
|
aria-live="polite"
|
||||||
|
></p>
|
||||||
|
|
||||||
|
<div class="AdminOverviewGroups">
|
||||||
|
<section>
|
||||||
|
<h2>Plants</h2>
|
||||||
|
<div id="AdminOverviewMissingSprites"></div>
|
||||||
|
<div id="AdminOverviewNoObtain"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Mutations</h2>
|
||||||
|
<div id="AdminOverviewNoDescription"></div>
|
||||||
|
<div id="AdminOverviewNoHint"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="/Scripts/Garden/PlantImages.js"></script>
|
||||||
|
<script src="/Scripts/Garden/AdminOverview.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$PageTitle = "Admin mass 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>Mass mutation editor</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Mark the mutations you want to edit, enable the fields to
|
||||||
|
change, then save. Fields left disabled stay untouched.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
id="AdminAccessMessage"
|
||||||
|
class="PageMessage"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
Checking admin access...
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="AdminContent" hidden>
|
||||||
|
<section class="Panel AdminMassSelectionPanel">
|
||||||
|
<div class="PanelHeader">
|
||||||
|
<h2>Mutations</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassSelectionBody">
|
||||||
|
<div
|
||||||
|
id="AdminMassMutationSelection"
|
||||||
|
class="AdminPlantChecklist AdminMassChecklist"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="AdminEditorActions">
|
||||||
|
<button
|
||||||
|
id="AdminMassMutationSelectAll"
|
||||||
|
class="ActionButton AdminInlineButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Select all
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="AdminMassMutationClear"
|
||||||
|
class="ActionButton AdminInlineButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Clear selection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="AdminMassMutationForm"
|
||||||
|
class="Panel AdminEditorForm AdminMassEditorForm"
|
||||||
|
>
|
||||||
|
<div class="PanelHeader PanelHeaderInset">
|
||||||
|
<h2>Fields</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminEditorGrid">
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Name">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Name</span>
|
||||||
|
<input id="AdminMassMutationName" type="text" maxlength="128">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Description">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Description</span>
|
||||||
|
<textarea id="AdminMassMutationDescription" rows="4"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Hint">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Hint</span>
|
||||||
|
<textarea id="AdminMassMutationHint" rows="3"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Priority">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Priority</span>
|
||||||
|
<input id="AdminMassMutationPriority" type="number" step="1" value="0">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Chance">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Chance (%)</span>
|
||||||
|
<input id="AdminMassMutationChance" type="number" min="0" max="100" step="0.01" value="100">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Cooldown">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<div class="AdminEditorField">
|
||||||
|
<span>Cooldown</span>
|
||||||
|
<div class="AdminDurationInputs">
|
||||||
|
<label>
|
||||||
|
<span>Hours</span>
|
||||||
|
<input id="AdminMassMutationCooldownHours" type="number" min="0" step="1" value="0">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Minutes</span>
|
||||||
|
<input id="AdminMassMutationCooldownMinutes" type="number" min="0" max="59" step="1" value="0">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Seconds</span>
|
||||||
|
<input id="AdminMassMutationCooldownSeconds" type="number" min="0" max="59" step="1" value="0">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Rotation">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Rotation</span>
|
||||||
|
<select id="AdminMassMutationRotation">
|
||||||
|
<option value="None">None</option>
|
||||||
|
<option value="Any">Any</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="AllowImmature">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Allow immature plants</span>
|
||||||
|
<select id="AdminMassMutationAllowImmature">
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Pattern">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Pattern JSON</span>
|
||||||
|
<textarea id="AdminMassMutationPattern" rows="7" spellcheck="false">[["Any"]]</textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Success">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Success result JSON</span>
|
||||||
|
<textarea id="AdminMassMutationSuccess" rows="7" spellcheck="false">[["Keep"]]</textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Failure">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Failure result JSON</span>
|
||||||
|
<textarea id="AdminMassMutationFailure" rows="7" spellcheck="false">"Keep"</textarea>
|
||||||
|
<small>Use "Keep", "Clear", or a result matrix.</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="PlantsUsed">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Plants used</span>
|
||||||
|
<input id="AdminMassMutationPlantsUsed" type="text" placeholder="RedRose, BlueRose">
|
||||||
|
<small>Comma-separated Plant Keys.</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="PlantsCreated">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Plants created</span>
|
||||||
|
<input id="AdminMassMutationPlantsCreated" type="text" placeholder="PurpleRose">
|
||||||
|
<small>Comma-separated Plant Keys.</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminEditorActions">
|
||||||
|
<button
|
||||||
|
id="AdminMassMutationSaveButton"
|
||||||
|
class="ActionButton AdminSaveButton"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save marked mutations
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="ActionButton AdminInlineButton AdminEditorBackLink"
|
||||||
|
href="/Pages/Admin.html"
|
||||||
|
>
|
||||||
|
Back to Admin
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p
|
||||||
|
id="AdminMassMutationMessage"
|
||||||
|
class="PageMessage"
|
||||||
|
aria-live="polite"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="/Scripts/Garden/Save.js"></script>
|
||||||
|
<script src="/Scripts/Garden/AdminValidation.js"></script>
|
||||||
|
<script src="/Scripts/Garden/AdminMassMutationEditor.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$PageTitle = "Admin mass 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>Mass plant editor</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Mark the plants you want to edit, enable the fields to
|
||||||
|
change, then save. Fields left disabled stay untouched.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
id="AdminAccessMessage"
|
||||||
|
class="PageMessage"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
Checking admin access...
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="AdminContent" hidden>
|
||||||
|
<section class="Panel AdminMassSelectionPanel">
|
||||||
|
<div class="PanelHeader">
|
||||||
|
<h2>Plants</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassSelectionBody">
|
||||||
|
<div
|
||||||
|
id="AdminMassPlantSelection"
|
||||||
|
class="AdminPlantChecklist AdminMassChecklist"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="AdminEditorActions">
|
||||||
|
<button
|
||||||
|
id="AdminMassPlantSelectAll"
|
||||||
|
class="ActionButton AdminInlineButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Select all
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="AdminMassPlantClear"
|
||||||
|
class="ActionButton AdminInlineButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Clear selection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="AdminMassPlantForm"
|
||||||
|
class="Panel AdminEditorForm AdminMassEditorForm"
|
||||||
|
>
|
||||||
|
<div class="PanelHeader PanelHeaderInset">
|
||||||
|
<h2>Fields</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminEditorGrid">
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Name">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Name</span>
|
||||||
|
<input id="AdminMassPlantName" type="text" maxlength="64">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Description">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Description</span>
|
||||||
|
<textarea id="AdminMassPlantDescription" rows="4"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Tags">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Tags</span>
|
||||||
|
<input id="AdminMassPlantTags" type="text" placeholder="Rose, Flower, Red">
|
||||||
|
<small>Comma-separated.</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="GrowthTime">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="AdminEditorField">
|
||||||
|
<span>Growth time</span>
|
||||||
|
|
||||||
|
<div class="AdminDurationInputs">
|
||||||
|
<label>
|
||||||
|
<span>Hours</span>
|
||||||
|
<input id="AdminMassPlantGrowthHours" type="number" min="0" step="1" value="1">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Minutes</span>
|
||||||
|
<input id="AdminMassPlantGrowthMinutes" type="number" min="0" max="59" step="1" value="0">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Seconds</span>
|
||||||
|
<input id="AdminMassPlantGrowthSeconds" type="number" min="0" max="59" step="1" value="0">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="HarvestMultiplier">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Harvest multiplier</span>
|
||||||
|
<input id="AdminMassPlantHarvestMultiplier" type="number" min="0" max="9999.9999" step="0.0001" value="1.5">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="ShopPlant">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Shop plant</span>
|
||||||
|
<select id="AdminMassPlantShopPlant">
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="BaseCost">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Base seed cost</span>
|
||||||
|
<input id="AdminMassPlantBaseCost" type="number" min="0" step="1" value="0">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminMassField AdminEditorWideField">
|
||||||
|
<label class="AdminEditorCheckField AdminMassApplyField">
|
||||||
|
<input type="checkbox" data-admin-mass-apply="Effects">
|
||||||
|
<span>Apply</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="AdminEditorField">
|
||||||
|
<span>Effects JSON</span>
|
||||||
|
<textarea id="AdminMassPlantEffects" rows="8" spellcheck="false">{}</textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminEditorActions">
|
||||||
|
<button
|
||||||
|
id="AdminMassPlantSaveButton"
|
||||||
|
class="ActionButton AdminSaveButton"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save marked plants
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="ActionButton AdminInlineButton AdminEditorBackLink"
|
||||||
|
href="/Pages/Admin.html"
|
||||||
|
>
|
||||||
|
Back to Admin
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p
|
||||||
|
id="AdminMassPlantMessage"
|
||||||
|
class="PageMessage"
|
||||||
|
aria-live="polite"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="/Scripts/Garden/Save.js"></script>
|
||||||
|
<script src="/Scripts/Garden/AdminValidation.js"></script>
|
||||||
|
<script src="/Scripts/Garden/AdminMassPlantEditor.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -64,6 +64,31 @@ $PageSection = "Admin";
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminEditorActions AdminEditorTransferActions">
|
||||||
|
<button
|
||||||
|
id="AdminMutationImportButton"
|
||||||
|
class="ActionButton AdminInlineButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Import JSON
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="AdminMutationImportFile"
|
||||||
|
type="file"
|
||||||
|
accept="application/json,.json"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="AdminMutationExportButton"
|
||||||
|
class="ActionButton AdminInlineButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Export JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
id="AdminMutationForm"
|
id="AdminMutationForm"
|
||||||
class="Panel AdminEditorForm"
|
class="Panel AdminEditorForm"
|
||||||
@@ -489,6 +514,7 @@ $PageSection = "Admin";
|
|||||||
<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/Mutations.js"></script>
|
||||||
<script src="/Scripts/Garden/Save.js"></script>
|
<script src="/Scripts/Garden/Save.js"></script>
|
||||||
|
<script src="/Scripts/Garden/AdminValidation.js"></script>
|
||||||
<script src="/Scripts/Garden/AdminMutationEditor.js"></script>
|
<script src="/Scripts/Garden/AdminMutationEditor.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -64,6 +64,31 @@ $PageSection = "Admin";
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="AdminEditorActions AdminEditorTransferActions">
|
||||||
|
<button
|
||||||
|
id="AdminPlantImportButton"
|
||||||
|
class="ActionButton AdminInlineButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Import JSON
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="AdminPlantImportFile"
|
||||||
|
type="file"
|
||||||
|
accept="application/json,.json"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="AdminPlantExportButton"
|
||||||
|
class="ActionButton AdminInlineButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Export JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
id="AdminPlantForm"
|
id="AdminPlantForm"
|
||||||
class="Panel AdminEditorForm"
|
class="Panel AdminEditorForm"
|
||||||
@@ -360,6 +385,7 @@ $PageSection = "Admin";
|
|||||||
<script src="/Scripts/Garden/Mutations.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/Economy.js"></script>
|
||||||
|
<script src="/Scripts/Garden/AdminValidation.js"></script>
|
||||||
<script src="/Scripts/Garden/AdminPlantEditor.js"></script>
|
<script src="/Scripts/Garden/AdminPlantEditor.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,787 @@
|
|||||||
|
let AdminMassMutationCatalogue = {};
|
||||||
|
let AdminMassMutationPlantCatalogue = {};
|
||||||
|
let AdminMassMutationSavePending = false;
|
||||||
|
|
||||||
|
|
||||||
|
async function AdminMassMutationRequest(
|
||||||
|
Endpoint,
|
||||||
|
Action,
|
||||||
|
Data = {}
|
||||||
|
) {
|
||||||
|
const SaveKey =
|
||||||
|
localStorage.getItem(
|
||||||
|
SaveKeyName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof SaveKey !== "string" ||
|
||||||
|
!/^[0-9A-Fa-f]{64}$/.test(
|
||||||
|
SaveKey
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"A valid Account Key is required."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Response = await fetch(
|
||||||
|
ApiUrl + "/" + Endpoint,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type":
|
||||||
|
"application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
SaveKey: SaveKey,
|
||||||
|
Action: Action,
|
||||||
|
...Data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Result =
|
||||||
|
await Response.json();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Response.ok ||
|
||||||
|
Result.Success !== true
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Result.Error ??
|
||||||
|
"Mutation admin request failed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetSortedAdminMassMutationKeys() {
|
||||||
|
return Object.keys(
|
||||||
|
AdminMassMutationCatalogue
|
||||||
|
).sort(
|
||||||
|
(A, B) =>
|
||||||
|
Number(
|
||||||
|
AdminMassMutationCatalogue[A]
|
||||||
|
?.Id ?? 0
|
||||||
|
) -
|
||||||
|
Number(
|
||||||
|
AdminMassMutationCatalogue[B]
|
||||||
|
?.Id ?? 0
|
||||||
|
) ||
|
||||||
|
A.localeCompare(B)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function RenderAdminMassMutationSelection() {
|
||||||
|
const Container =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationSelection"
|
||||||
|
);
|
||||||
|
|
||||||
|
Container.replaceChildren();
|
||||||
|
|
||||||
|
for (
|
||||||
|
const MutationKey
|
||||||
|
of GetSortedAdminMassMutationKeys()
|
||||||
|
) {
|
||||||
|
const Mutation =
|
||||||
|
AdminMassMutationCatalogue[
|
||||||
|
MutationKey
|
||||||
|
];
|
||||||
|
|
||||||
|
const Label =
|
||||||
|
document.createElement("label");
|
||||||
|
Label.className =
|
||||||
|
"AdminPlantChecklistItem";
|
||||||
|
|
||||||
|
const Input =
|
||||||
|
document.createElement("input");
|
||||||
|
Input.type = "checkbox";
|
||||||
|
Input.value = MutationKey;
|
||||||
|
Input.dataset.adminMassTarget =
|
||||||
|
"mutation";
|
||||||
|
|
||||||
|
const Text =
|
||||||
|
document.createElement("span");
|
||||||
|
Text.textContent =
|
||||||
|
String(Mutation.Id).padStart(
|
||||||
|
3,
|
||||||
|
"0"
|
||||||
|
) +
|
||||||
|
" — " +
|
||||||
|
(Mutation.Name ?? MutationKey) +
|
||||||
|
(
|
||||||
|
Mutation.Archived === true
|
||||||
|
? " (archived)"
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
|
||||||
|
Label.append(Input, Text);
|
||||||
|
Container.appendChild(Label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function SetAdminMassMutationSelection(
|
||||||
|
Checked
|
||||||
|
) {
|
||||||
|
for (
|
||||||
|
const Input
|
||||||
|
of document.querySelectorAll(
|
||||||
|
'[data-admin-mass-target="mutation"]'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Input.checked = Checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function UpdateAdminMassMutationFieldStates() {
|
||||||
|
const FieldMap = {
|
||||||
|
Name: ["AdminMassMutationName"],
|
||||||
|
Description: [
|
||||||
|
"AdminMassMutationDescription"
|
||||||
|
],
|
||||||
|
Hint: ["AdminMassMutationHint"],
|
||||||
|
Priority: [
|
||||||
|
"AdminMassMutationPriority"
|
||||||
|
],
|
||||||
|
Chance: ["AdminMassMutationChance"],
|
||||||
|
Cooldown: [
|
||||||
|
"AdminMassMutationCooldownHours",
|
||||||
|
"AdminMassMutationCooldownMinutes",
|
||||||
|
"AdminMassMutationCooldownSeconds"
|
||||||
|
],
|
||||||
|
Rotation: [
|
||||||
|
"AdminMassMutationRotation"
|
||||||
|
],
|
||||||
|
AllowImmature: [
|
||||||
|
"AdminMassMutationAllowImmature"
|
||||||
|
],
|
||||||
|
Pattern: [
|
||||||
|
"AdminMassMutationPattern"
|
||||||
|
],
|
||||||
|
Success: [
|
||||||
|
"AdminMassMutationSuccess"
|
||||||
|
],
|
||||||
|
Failure: [
|
||||||
|
"AdminMassMutationFailure"
|
||||||
|
],
|
||||||
|
PlantsUsed: [
|
||||||
|
"AdminMassMutationPlantsUsed"
|
||||||
|
],
|
||||||
|
PlantsCreated: [
|
||||||
|
"AdminMassMutationPlantsCreated"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
for (
|
||||||
|
const Apply
|
||||||
|
of document.querySelectorAll(
|
||||||
|
"[data-admin-mass-apply]"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const FieldName =
|
||||||
|
Apply.dataset.adminMassApply;
|
||||||
|
|
||||||
|
for (
|
||||||
|
const ElementId
|
||||||
|
of FieldMap[FieldName] ?? []
|
||||||
|
) {
|
||||||
|
document.getElementById(
|
||||||
|
ElementId
|
||||||
|
).disabled = !Apply.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function AdminMassMutationFieldEnabled(
|
||||||
|
FieldName
|
||||||
|
) {
|
||||||
|
return document.querySelector(
|
||||||
|
'[data-admin-mass-apply="' +
|
||||||
|
FieldName +
|
||||||
|
'"]'
|
||||||
|
)?.checked === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminMassMutationSelectedKeys() {
|
||||||
|
return [
|
||||||
|
...document.querySelectorAll(
|
||||||
|
'[data-admin-mass-target="mutation"]:checked'
|
||||||
|
)
|
||||||
|
].map(Input => Input.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminMassMutationCooldown() {
|
||||||
|
const Hours = Number(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationCooldownHours"
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
const Minutes = Number(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationCooldownMinutes"
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
const Seconds = Number(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationCooldownSeconds"
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isInteger(Hours) ||
|
||||||
|
!Number.isInteger(Minutes) ||
|
||||||
|
!Number.isInteger(Seconds) ||
|
||||||
|
Hours < 0 ||
|
||||||
|
Minutes < 0 ||
|
||||||
|
Minutes > 59 ||
|
||||||
|
Seconds < 0 ||
|
||||||
|
Seconds > 59
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Cooldown is invalid."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
Hours * 3600000 +
|
||||||
|
Minutes * 60000 +
|
||||||
|
Seconds * 1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ParseAdminMassMutationJson(
|
||||||
|
ElementId,
|
||||||
|
Name
|
||||||
|
) {
|
||||||
|
const Raw =
|
||||||
|
document.getElementById(
|
||||||
|
ElementId
|
||||||
|
).value.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(Raw);
|
||||||
|
} catch (Error) {
|
||||||
|
throw new Error(
|
||||||
|
Name + " is not valid JSON."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ParseAdminMassMutationPlantKeys(
|
||||||
|
ElementId,
|
||||||
|
Name
|
||||||
|
) {
|
||||||
|
const Keys = [
|
||||||
|
...new Set(
|
||||||
|
document.getElementById(
|
||||||
|
ElementId
|
||||||
|
).value
|
||||||
|
.split(",")
|
||||||
|
.map(Value => Value.trim())
|
||||||
|
.filter(Value => Value !== "")
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
return Keys.map(
|
||||||
|
PlantKey => {
|
||||||
|
const Plant =
|
||||||
|
AdminMassMutationPlantCatalogue[
|
||||||
|
PlantKey
|
||||||
|
];
|
||||||
|
|
||||||
|
if (Plant === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
Name +
|
||||||
|
" references unknown Plant Key: " +
|
||||||
|
PlantKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(Plant.Id);
|
||||||
|
}
|
||||||
|
).sort((A, B) => A - B);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminMassMutationPatch() {
|
||||||
|
const Patch = {};
|
||||||
|
|
||||||
|
if (AdminMassMutationFieldEnabled("Name")) {
|
||||||
|
Patch.Name =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationName"
|
||||||
|
).value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"Description"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.Description =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationDescription"
|
||||||
|
).value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AdminMassMutationFieldEnabled("Hint")) {
|
||||||
|
Patch.Hint =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationHint"
|
||||||
|
).value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"Priority"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.Priority =
|
||||||
|
Number(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationPriority"
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"Chance"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.Chance =
|
||||||
|
Number(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationChance"
|
||||||
|
).value
|
||||||
|
) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"Cooldown"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.Cooldown =
|
||||||
|
GetAdminMassMutationCooldown();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"Rotation"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.Rotation =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationRotation"
|
||||||
|
).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"AllowImmature"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.AllowImmature =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationAllowImmature"
|
||||||
|
).value === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"Pattern"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.Pattern =
|
||||||
|
ParseAdminMassMutationJson(
|
||||||
|
"AdminMassMutationPattern",
|
||||||
|
"Pattern JSON"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"Success"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.Success =
|
||||||
|
ParseAdminMassMutationJson(
|
||||||
|
"AdminMassMutationSuccess",
|
||||||
|
"Success result JSON"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"Failure"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.Failure =
|
||||||
|
ParseAdminMassMutationJson(
|
||||||
|
"AdminMassMutationFailure",
|
||||||
|
"Failure result JSON"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"PlantsUsed"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.PlantsUsed =
|
||||||
|
ParseAdminMassMutationPlantKeys(
|
||||||
|
"AdminMassMutationPlantsUsed",
|
||||||
|
"Plants used"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassMutationFieldEnabled(
|
||||||
|
"PlantsCreated"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.PlantsCreated =
|
||||||
|
ParseAdminMassMutationPlantKeys(
|
||||||
|
"AdminMassMutationPlantsCreated",
|
||||||
|
"Plants created"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function BuildAdminMassMutationPayload(
|
||||||
|
MutationKey,
|
||||||
|
Patch
|
||||||
|
) {
|
||||||
|
const Source =
|
||||||
|
AdminMassMutationCatalogue[
|
||||||
|
MutationKey
|
||||||
|
];
|
||||||
|
|
||||||
|
const Mutation = {
|
||||||
|
Id: Number(Source.Id),
|
||||||
|
MutationKey: MutationKey,
|
||||||
|
Name: Source.Name ?? "",
|
||||||
|
Description:
|
||||||
|
Source.Description ?? "",
|
||||||
|
Hint: Source.Hint ?? "",
|
||||||
|
Priority:
|
||||||
|
Number(Source.Priority ?? 0),
|
||||||
|
Chance:
|
||||||
|
Number(Source.Chance ?? 0),
|
||||||
|
Cooldown:
|
||||||
|
Number(Source.Cooldown ?? 0),
|
||||||
|
Rotation:
|
||||||
|
Source.Rotation ?? "None",
|
||||||
|
AllowImmature:
|
||||||
|
Source.AllowImmature === true,
|
||||||
|
Pattern:
|
||||||
|
JSON.parse(
|
||||||
|
JSON.stringify(
|
||||||
|
Source.Pattern ?? []
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Success:
|
||||||
|
JSON.parse(
|
||||||
|
JSON.stringify(
|
||||||
|
Source.Success ?? []
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Failure:
|
||||||
|
JSON.parse(
|
||||||
|
JSON.stringify(
|
||||||
|
Source.Failure ?? "Keep"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Relations: {
|
||||||
|
PlantsUsed: [
|
||||||
|
...(Source.Relations
|
||||||
|
?.PlantsUsed ?? [])
|
||||||
|
],
|
||||||
|
PlantsCreated: [
|
||||||
|
...(Source.Relations
|
||||||
|
?.PlantsCreated ?? [])
|
||||||
|
]
|
||||||
|
},
|
||||||
|
IsNew: false
|
||||||
|
};
|
||||||
|
|
||||||
|
for (
|
||||||
|
const [Key, Value]
|
||||||
|
of Object.entries(Patch)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
Key === "PlantsUsed" ||
|
||||||
|
Key === "PlantsCreated"
|
||||||
|
) {
|
||||||
|
Mutation.Relations[Key] = [
|
||||||
|
...Value
|
||||||
|
];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mutation[Key] =
|
||||||
|
Value !== null &&
|
||||||
|
typeof Value === "object"
|
||||||
|
? JSON.parse(
|
||||||
|
JSON.stringify(Value)
|
||||||
|
)
|
||||||
|
: Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function SaveAdminMassMutations(
|
||||||
|
Event
|
||||||
|
) {
|
||||||
|
Event.preventDefault();
|
||||||
|
|
||||||
|
if (AdminMassMutationSavePending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectedKeys =
|
||||||
|
GetAdminMassMutationSelectedKeys();
|
||||||
|
|
||||||
|
if (SelectedKeys.length === 0) {
|
||||||
|
SetAdminMassMutationMessage(
|
||||||
|
"Mark at least one mutation first."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Patch;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Patch =
|
||||||
|
GetAdminMassMutationPatch();
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminMassMutationMessage(
|
||||||
|
Error.message
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(Patch).length === 0) {
|
||||||
|
SetAdminMassMutationMessage(
|
||||||
|
"Enable at least one field to update."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Payloads = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (
|
||||||
|
const MutationKey
|
||||||
|
of SelectedKeys
|
||||||
|
) {
|
||||||
|
const Mutation =
|
||||||
|
BuildAdminMassMutationPayload(
|
||||||
|
MutationKey,
|
||||||
|
Patch
|
||||||
|
);
|
||||||
|
|
||||||
|
ValidateAdminMutation(
|
||||||
|
Mutation,
|
||||||
|
AdminMassMutationCatalogue,
|
||||||
|
AdminMassMutationPlantCatalogue,
|
||||||
|
MutationKey
|
||||||
|
);
|
||||||
|
|
||||||
|
Payloads.push(Mutation);
|
||||||
|
}
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminMassMutationMessage(
|
||||||
|
Error.message
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminMassMutationSavePending = true;
|
||||||
|
|
||||||
|
const SaveButton =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationSaveButton"
|
||||||
|
);
|
||||||
|
|
||||||
|
SaveButton.disabled = true;
|
||||||
|
|
||||||
|
let SavedCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const Mutation of Payloads) {
|
||||||
|
SetAdminMassMutationMessage(
|
||||||
|
"Saving " +
|
||||||
|
(SavedCount + 1) +
|
||||||
|
" of " +
|
||||||
|
Payloads.length +
|
||||||
|
"..."
|
||||||
|
);
|
||||||
|
|
||||||
|
await AdminMassMutationRequest(
|
||||||
|
"AdminMutations.php",
|
||||||
|
"Save",
|
||||||
|
{Mutation: Mutation}
|
||||||
|
);
|
||||||
|
|
||||||
|
SavedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem(
|
||||||
|
"SarahtoninGardenContent"
|
||||||
|
);
|
||||||
|
|
||||||
|
await LoadAdminMassMutationData();
|
||||||
|
|
||||||
|
SetAdminMassMutationMessage(
|
||||||
|
SavedCount === 1
|
||||||
|
? "1 mutation updated."
|
||||||
|
: SavedCount +
|
||||||
|
" mutations updated."
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminMassMutationMessage(
|
||||||
|
"Saved " +
|
||||||
|
SavedCount +
|
||||||
|
" before stopping: " +
|
||||||
|
(Error.message ??
|
||||||
|
"Couldn't save mutations.")
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
AdminMassMutationSavePending = false;
|
||||||
|
SaveButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function BindAdminMassMutationEditor() {
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationSelectAll"
|
||||||
|
).addEventListener(
|
||||||
|
"click",
|
||||||
|
() =>
|
||||||
|
SetAdminMassMutationSelection(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationClear"
|
||||||
|
).addEventListener(
|
||||||
|
"click",
|
||||||
|
() =>
|
||||||
|
SetAdminMassMutationSelection(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationForm"
|
||||||
|
).addEventListener(
|
||||||
|
"submit",
|
||||||
|
SaveAdminMassMutations
|
||||||
|
);
|
||||||
|
|
||||||
|
for (
|
||||||
|
const Apply
|
||||||
|
of document.querySelectorAll(
|
||||||
|
"[data-admin-mass-apply]"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Apply.addEventListener(
|
||||||
|
"change",
|
||||||
|
UpdateAdminMassMutationFieldStates
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateAdminMassMutationFieldStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function LoadAdminMassMutationData() {
|
||||||
|
const [MutationResult, PlantResult] =
|
||||||
|
await Promise.all([
|
||||||
|
AdminMassMutationRequest(
|
||||||
|
"AdminMutations.php",
|
||||||
|
"List"
|
||||||
|
),
|
||||||
|
AdminMassMutationRequest(
|
||||||
|
"AdminPlants.php",
|
||||||
|
"List"
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
AdminMassMutationCatalogue =
|
||||||
|
MutationResult.Mutations ?? {};
|
||||||
|
AdminMassMutationPlantCatalogue =
|
||||||
|
PlantResult.Plants ?? {};
|
||||||
|
|
||||||
|
RenderAdminMassMutationSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function SetAdminMassMutationMessage(
|
||||||
|
Message
|
||||||
|
) {
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassMutationMessage"
|
||||||
|
).textContent = Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function StartAdminMassMutationEditor() {
|
||||||
|
try {
|
||||||
|
const Status =
|
||||||
|
await GetAdminStatus();
|
||||||
|
|
||||||
|
if (
|
||||||
|
Status.Success !== true ||
|
||||||
|
Status.IsAdmin !== true
|
||||||
|
) {
|
||||||
|
SetAdminMassMutationMessage(
|
||||||
|
"Admin access required."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BindAdminMassMutationEditor();
|
||||||
|
await LoadAdminMassMutationData();
|
||||||
|
|
||||||
|
SetAdminMassMutationMessage(
|
||||||
|
"Mark mutations and enable the fields you want to change."
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
console.error(
|
||||||
|
"Couldn't start Mass mutation editor:",
|
||||||
|
Error
|
||||||
|
);
|
||||||
|
|
||||||
|
SetAdminMassMutationMessage(
|
||||||
|
Error.message ??
|
||||||
|
"Couldn't load the Mass mutation editor."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"DOMContentLoaded",
|
||||||
|
StartAdminMassMutationEditor
|
||||||
|
);
|
||||||
@@ -0,0 +1,645 @@
|
|||||||
|
let AdminMassPlantCatalogue = {};
|
||||||
|
let AdminMassPlantSavePending = false;
|
||||||
|
|
||||||
|
|
||||||
|
async function AdminMassPlantRequest(
|
||||||
|
Action,
|
||||||
|
Data = {}
|
||||||
|
) {
|
||||||
|
const SaveKey =
|
||||||
|
localStorage.getItem(
|
||||||
|
SaveKeyName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof SaveKey !== "string" ||
|
||||||
|
!/^[0-9A-Fa-f]{64}$/.test(
|
||||||
|
SaveKey
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"A valid Account Key is required."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Response = await fetch(
|
||||||
|
ApiUrl + "/AdminPlants.php",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type":
|
||||||
|
"application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
SaveKey: SaveKey,
|
||||||
|
Action: Action,
|
||||||
|
...Data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Result =
|
||||||
|
await Response.json();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Response.ok ||
|
||||||
|
Result.Success !== true
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Result.Error ??
|
||||||
|
"Plant admin request failed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetSortedAdminMassPlantKeys() {
|
||||||
|
return Object.keys(
|
||||||
|
AdminMassPlantCatalogue
|
||||||
|
).sort(
|
||||||
|
(A, B) =>
|
||||||
|
Number(
|
||||||
|
AdminMassPlantCatalogue[A]
|
||||||
|
?.Id ?? 0
|
||||||
|
) -
|
||||||
|
Number(
|
||||||
|
AdminMassPlantCatalogue[B]
|
||||||
|
?.Id ?? 0
|
||||||
|
) ||
|
||||||
|
A.localeCompare(B)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function RenderAdminMassPlantSelection() {
|
||||||
|
const Container =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantSelection"
|
||||||
|
);
|
||||||
|
|
||||||
|
Container.replaceChildren();
|
||||||
|
|
||||||
|
for (
|
||||||
|
const PlantKey
|
||||||
|
of GetSortedAdminMassPlantKeys()
|
||||||
|
) {
|
||||||
|
const Plant =
|
||||||
|
AdminMassPlantCatalogue[
|
||||||
|
PlantKey
|
||||||
|
];
|
||||||
|
|
||||||
|
const Label =
|
||||||
|
document.createElement("label");
|
||||||
|
Label.className =
|
||||||
|
"AdminPlantChecklistItem";
|
||||||
|
|
||||||
|
const Input =
|
||||||
|
document.createElement("input");
|
||||||
|
Input.type = "checkbox";
|
||||||
|
Input.value = PlantKey;
|
||||||
|
Input.dataset.adminMassTarget =
|
||||||
|
"plant";
|
||||||
|
|
||||||
|
const Text =
|
||||||
|
document.createElement("span");
|
||||||
|
Text.textContent =
|
||||||
|
String(Plant.Id).padStart(
|
||||||
|
3,
|
||||||
|
"0"
|
||||||
|
) +
|
||||||
|
" — " +
|
||||||
|
(Plant.Name ?? PlantKey) +
|
||||||
|
(
|
||||||
|
Plant.Archived === true
|
||||||
|
? " (archived)"
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
|
||||||
|
Label.append(
|
||||||
|
Input,
|
||||||
|
Text
|
||||||
|
);
|
||||||
|
Container.appendChild(Label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function SetAdminMassPlantSelection(
|
||||||
|
Checked
|
||||||
|
) {
|
||||||
|
for (
|
||||||
|
const Input
|
||||||
|
of document.querySelectorAll(
|
||||||
|
'[data-admin-mass-target="plant"]'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Input.checked = Checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function UpdateAdminMassPlantFieldStates() {
|
||||||
|
const FieldMap = {
|
||||||
|
Name: [
|
||||||
|
"AdminMassPlantName"
|
||||||
|
],
|
||||||
|
Description: [
|
||||||
|
"AdminMassPlantDescription"
|
||||||
|
],
|
||||||
|
Tags: [
|
||||||
|
"AdminMassPlantTags"
|
||||||
|
],
|
||||||
|
GrowthTime: [
|
||||||
|
"AdminMassPlantGrowthHours",
|
||||||
|
"AdminMassPlantGrowthMinutes",
|
||||||
|
"AdminMassPlantGrowthSeconds"
|
||||||
|
],
|
||||||
|
HarvestMultiplier: [
|
||||||
|
"AdminMassPlantHarvestMultiplier"
|
||||||
|
],
|
||||||
|
ShopPlant: [
|
||||||
|
"AdminMassPlantShopPlant"
|
||||||
|
],
|
||||||
|
BaseCost: [
|
||||||
|
"AdminMassPlantBaseCost"
|
||||||
|
],
|
||||||
|
Effects: [
|
||||||
|
"AdminMassPlantEffects"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
for (
|
||||||
|
const Apply
|
||||||
|
of document.querySelectorAll(
|
||||||
|
"[data-admin-mass-apply]"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const FieldName =
|
||||||
|
Apply.dataset.adminMassApply;
|
||||||
|
|
||||||
|
for (
|
||||||
|
const ElementId
|
||||||
|
of FieldMap[FieldName] ?? []
|
||||||
|
) {
|
||||||
|
document.getElementById(
|
||||||
|
ElementId
|
||||||
|
).disabled = !Apply.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminMassPlantSelectedKeys() {
|
||||||
|
return [
|
||||||
|
...document.querySelectorAll(
|
||||||
|
'[data-admin-mass-target="plant"]:checked'
|
||||||
|
)
|
||||||
|
].map(Input => Input.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function AdminMassPlantFieldEnabled(
|
||||||
|
FieldName
|
||||||
|
) {
|
||||||
|
return document.querySelector(
|
||||||
|
'[data-admin-mass-apply="' +
|
||||||
|
FieldName +
|
||||||
|
'"]'
|
||||||
|
)?.checked === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ParseAdminMassPlantTags() {
|
||||||
|
return [
|
||||||
|
...new Set(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantTags"
|
||||||
|
).value
|
||||||
|
.split(",")
|
||||||
|
.map(Tag => Tag.trim())
|
||||||
|
.filter(Tag => Tag !== "")
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminMassPlantGrowthTime() {
|
||||||
|
const Hours = Number(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantGrowthHours"
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
const Minutes = Number(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantGrowthMinutes"
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
const Seconds = Number(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantGrowthSeconds"
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isInteger(Hours) ||
|
||||||
|
!Number.isInteger(Minutes) ||
|
||||||
|
!Number.isInteger(Seconds) ||
|
||||||
|
Hours < 0 ||
|
||||||
|
Minutes < 0 ||
|
||||||
|
Minutes > 59 ||
|
||||||
|
Seconds < 0 ||
|
||||||
|
Seconds > 59
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Growth time is invalid."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
Hours * 3600000 +
|
||||||
|
Minutes * 60000 +
|
||||||
|
Seconds * 1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminMassPlantEffects() {
|
||||||
|
const Raw =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantEffects"
|
||||||
|
).value.trim();
|
||||||
|
|
||||||
|
const Effects = JSON.parse(
|
||||||
|
Raw === "" ? "{}" : Raw
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!IsAdminPlainObject(Effects)) {
|
||||||
|
throw new Error(
|
||||||
|
"Effects JSON must be an object."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Effects;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminMassPlantPatch() {
|
||||||
|
const Patch = {};
|
||||||
|
|
||||||
|
if (AdminMassPlantFieldEnabled("Name")) {
|
||||||
|
Patch.Name =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantName"
|
||||||
|
).value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassPlantFieldEnabled(
|
||||||
|
"Description"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.Description =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantDescription"
|
||||||
|
).value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AdminMassPlantFieldEnabled("Tags")) {
|
||||||
|
Patch.Tags =
|
||||||
|
ParseAdminMassPlantTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassPlantFieldEnabled(
|
||||||
|
"GrowthTime"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.GrowthTime =
|
||||||
|
GetAdminMassPlantGrowthTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassPlantFieldEnabled(
|
||||||
|
"HarvestMultiplier"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.HarvestMultiplier =
|
||||||
|
Number(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantHarvestMultiplier"
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassPlantFieldEnabled(
|
||||||
|
"ShopPlant"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.ShopPlant =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantShopPlant"
|
||||||
|
).value === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassPlantFieldEnabled(
|
||||||
|
"BaseCost"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.BaseCost =
|
||||||
|
Number(
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantBaseCost"
|
||||||
|
).value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AdminMassPlantFieldEnabled(
|
||||||
|
"Effects"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Patch.Effects =
|
||||||
|
GetAdminMassPlantEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function BuildAdminMassPlantPayload(
|
||||||
|
PlantKey,
|
||||||
|
Patch
|
||||||
|
) {
|
||||||
|
const Source =
|
||||||
|
AdminMassPlantCatalogue[
|
||||||
|
PlantKey
|
||||||
|
];
|
||||||
|
|
||||||
|
const Plant = {
|
||||||
|
Id: Number(Source.Id),
|
||||||
|
PlantKey: PlantKey,
|
||||||
|
Name: Source.Name ?? "",
|
||||||
|
Description:
|
||||||
|
Source.Description ?? "",
|
||||||
|
Tags:
|
||||||
|
Array.isArray(Source.Tags)
|
||||||
|
? [...Source.Tags]
|
||||||
|
: [],
|
||||||
|
GrowthTime:
|
||||||
|
Number(Source.GrowthTime ?? 0),
|
||||||
|
HarvestMultiplier:
|
||||||
|
Number(
|
||||||
|
Source.HarvestMultiplier ??
|
||||||
|
1.5
|
||||||
|
),
|
||||||
|
Effects:
|
||||||
|
JSON.parse(
|
||||||
|
JSON.stringify(
|
||||||
|
Source.Effects ?? {}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ShopPlant:
|
||||||
|
Source.Shop?.ShopPlant === true,
|
||||||
|
BaseCost:
|
||||||
|
Source.Shop?.BaseCost ?? null,
|
||||||
|
IsNew: false
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
Plant,
|
||||||
|
JSON.parse(
|
||||||
|
JSON.stringify(Patch)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Plant;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function SaveAdminMassPlants(
|
||||||
|
Event
|
||||||
|
) {
|
||||||
|
Event.preventDefault();
|
||||||
|
|
||||||
|
if (AdminMassPlantSavePending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectedKeys =
|
||||||
|
GetAdminMassPlantSelectedKeys();
|
||||||
|
|
||||||
|
if (SelectedKeys.length === 0) {
|
||||||
|
SetAdminMassPlantMessage(
|
||||||
|
"Mark at least one plant first."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Patch;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Patch = GetAdminMassPlantPatch();
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminMassPlantMessage(
|
||||||
|
Error.message
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(Patch).length === 0) {
|
||||||
|
SetAdminMassPlantMessage(
|
||||||
|
"Enable at least one field to update."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Payloads = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const PlantKey of SelectedKeys) {
|
||||||
|
const Plant =
|
||||||
|
BuildAdminMassPlantPayload(
|
||||||
|
PlantKey,
|
||||||
|
Patch
|
||||||
|
);
|
||||||
|
|
||||||
|
ValidateAdminPlant(
|
||||||
|
Plant,
|
||||||
|
AdminMassPlantCatalogue,
|
||||||
|
PlantKey
|
||||||
|
);
|
||||||
|
|
||||||
|
Payloads.push(Plant);
|
||||||
|
}
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminMassPlantMessage(
|
||||||
|
Error.message
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminMassPlantSavePending = true;
|
||||||
|
|
||||||
|
const SaveButton =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantSaveButton"
|
||||||
|
);
|
||||||
|
|
||||||
|
SaveButton.disabled = true;
|
||||||
|
|
||||||
|
let SavedCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const Plant of Payloads) {
|
||||||
|
SetAdminMassPlantMessage(
|
||||||
|
"Saving " +
|
||||||
|
(SavedCount + 1) +
|
||||||
|
" of " +
|
||||||
|
Payloads.length +
|
||||||
|
"..."
|
||||||
|
);
|
||||||
|
|
||||||
|
await AdminMassPlantRequest(
|
||||||
|
"Save",
|
||||||
|
{Plant: Plant}
|
||||||
|
);
|
||||||
|
|
||||||
|
SavedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem(
|
||||||
|
"SarahtoninGardenContent"
|
||||||
|
);
|
||||||
|
|
||||||
|
await LoadAdminMassPlants();
|
||||||
|
|
||||||
|
SetAdminMassPlantMessage(
|
||||||
|
SavedCount === 1
|
||||||
|
? "1 plant updated."
|
||||||
|
: SavedCount +
|
||||||
|
" plants updated."
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminMassPlantMessage(
|
||||||
|
"Saved " +
|
||||||
|
SavedCount +
|
||||||
|
" before stopping: " +
|
||||||
|
(Error.message ??
|
||||||
|
"Couldn't save plants.")
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
AdminMassPlantSavePending = false;
|
||||||
|
SaveButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function BindAdminMassPlantEditor() {
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantSelectAll"
|
||||||
|
).addEventListener(
|
||||||
|
"click",
|
||||||
|
() =>
|
||||||
|
SetAdminMassPlantSelection(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantClear"
|
||||||
|
).addEventListener(
|
||||||
|
"click",
|
||||||
|
() =>
|
||||||
|
SetAdminMassPlantSelection(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantForm"
|
||||||
|
).addEventListener(
|
||||||
|
"submit",
|
||||||
|
SaveAdminMassPlants
|
||||||
|
);
|
||||||
|
|
||||||
|
for (
|
||||||
|
const Apply
|
||||||
|
of document.querySelectorAll(
|
||||||
|
"[data-admin-mass-apply]"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Apply.addEventListener(
|
||||||
|
"change",
|
||||||
|
UpdateAdminMassPlantFieldStates
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateAdminMassPlantFieldStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function LoadAdminMassPlants() {
|
||||||
|
const Result =
|
||||||
|
await AdminMassPlantRequest(
|
||||||
|
"List"
|
||||||
|
);
|
||||||
|
|
||||||
|
AdminMassPlantCatalogue =
|
||||||
|
Result.Plants ?? {};
|
||||||
|
|
||||||
|
RenderAdminMassPlantSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function SetAdminMassPlantMessage(
|
||||||
|
Message
|
||||||
|
) {
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMassPlantMessage"
|
||||||
|
).textContent = Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function StartAdminMassPlantEditor() {
|
||||||
|
try {
|
||||||
|
const Status =
|
||||||
|
await GetAdminStatus();
|
||||||
|
|
||||||
|
if (
|
||||||
|
Status.Success !== true ||
|
||||||
|
Status.IsAdmin !== true
|
||||||
|
) {
|
||||||
|
SetAdminMassPlantMessage(
|
||||||
|
"Admin access required."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BindAdminMassPlantEditor();
|
||||||
|
await LoadAdminMassPlants();
|
||||||
|
|
||||||
|
SetAdminMassPlantMessage(
|
||||||
|
"Mark plants and enable the fields you want to change."
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
console.error(
|
||||||
|
"Couldn't start Mass plant editor:",
|
||||||
|
Error
|
||||||
|
);
|
||||||
|
|
||||||
|
SetAdminMassPlantMessage(
|
||||||
|
Error.message ??
|
||||||
|
"Couldn't load the Mass plant editor."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"DOMContentLoaded",
|
||||||
|
StartAdminMassPlantEditor
|
||||||
|
);
|
||||||
@@ -80,6 +80,31 @@ function BindAdminMutationEditor() {
|
|||||||
DuplicateAdminMutation
|
DuplicateAdminMutation
|
||||||
);
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMutationImportButton"
|
||||||
|
).addEventListener(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMutationImportFile"
|
||||||
|
).click();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMutationImportFile"
|
||||||
|
).addEventListener(
|
||||||
|
"change",
|
||||||
|
ImportAdminMutationJson
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMutationExportButton"
|
||||||
|
).addEventListener(
|
||||||
|
"click",
|
||||||
|
ExportAdminMutationJson
|
||||||
|
);
|
||||||
|
|
||||||
document.getElementById(
|
document.getElementById(
|
||||||
"AdminMutationResizeButton"
|
"AdminMutationResizeButton"
|
||||||
).addEventListener(
|
).addEventListener(
|
||||||
@@ -2339,6 +2364,21 @@ async function SaveAdminMutation(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ValidateAdminMutation(
|
||||||
|
Mutation,
|
||||||
|
AdminMutationCatalogue,
|
||||||
|
AdminMutationPlantCatalogue,
|
||||||
|
AdminMutationEditingKey
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminMutationMessage(
|
||||||
|
Error.message
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AdminMutationSavePending = true;
|
AdminMutationSavePending = true;
|
||||||
|
|
||||||
const SaveButton =
|
const SaveButton =
|
||||||
@@ -2419,6 +2459,458 @@ function RenderAdminMutationJsonPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminMutationPortableRelations(
|
||||||
|
Relations
|
||||||
|
) {
|
||||||
|
const Result = {
|
||||||
|
PlantsUsed: [],
|
||||||
|
PlantsCreated: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (
|
||||||
|
const RelationName
|
||||||
|
of [
|
||||||
|
"PlantsUsed",
|
||||||
|
"PlantsCreated"
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
for (
|
||||||
|
const PlantId
|
||||||
|
of Relations?.[
|
||||||
|
RelationName
|
||||||
|
] ?? []
|
||||||
|
) {
|
||||||
|
const PlantKey =
|
||||||
|
GetAdminPlantKeyById(
|
||||||
|
PlantId,
|
||||||
|
AdminMutationPlantCatalogue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (PlantKey !== null) {
|
||||||
|
Result[RelationName].push(
|
||||||
|
PlantKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminMutationPortableData(
|
||||||
|
Mutation
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
MutationKey:
|
||||||
|
Mutation.MutationKey,
|
||||||
|
Name: Mutation.Name,
|
||||||
|
Description:
|
||||||
|
Mutation.Description,
|
||||||
|
Hint:
|
||||||
|
Mutation.Hint === ""
|
||||||
|
? null
|
||||||
|
: Mutation.Hint,
|
||||||
|
Priority: Mutation.Priority,
|
||||||
|
Chance: Mutation.Chance,
|
||||||
|
Cooldown: Mutation.Cooldown,
|
||||||
|
Rotation: Mutation.Rotation,
|
||||||
|
AllowImmature:
|
||||||
|
Mutation.AllowImmature === true,
|
||||||
|
Pattern:
|
||||||
|
CloneAdminMutationValue(
|
||||||
|
Mutation.Pattern
|
||||||
|
),
|
||||||
|
Success:
|
||||||
|
CloneAdminMutationValue(
|
||||||
|
Mutation.Success
|
||||||
|
),
|
||||||
|
Failure:
|
||||||
|
CloneAdminMutationValue(
|
||||||
|
Mutation.Failure
|
||||||
|
),
|
||||||
|
Relations:
|
||||||
|
GetAdminMutationPortableRelations(
|
||||||
|
Mutation.Relations
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ExportAdminMutationJson() {
|
||||||
|
const Form =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMutationForm"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!Form.reportValidity()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const Mutation =
|
||||||
|
GetAdminMutationFormData();
|
||||||
|
|
||||||
|
ValidateAdminMutation(
|
||||||
|
Mutation,
|
||||||
|
AdminMutationCatalogue,
|
||||||
|
AdminMutationPlantCatalogue,
|
||||||
|
AdminMutationEditingKey
|
||||||
|
);
|
||||||
|
|
||||||
|
DownloadAdminJson(
|
||||||
|
"Mutation-" +
|
||||||
|
Mutation.MutationKey +
|
||||||
|
".json",
|
||||||
|
{
|
||||||
|
Type: "Mutation",
|
||||||
|
Version: 1,
|
||||||
|
Mutation:
|
||||||
|
GetAdminMutationPortableData(
|
||||||
|
Mutation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
SetAdminMutationMessage(
|
||||||
|
"Mutation JSON exported. Numeric IDs are not included."
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminMutationMessage(
|
||||||
|
Error.message ??
|
||||||
|
"Couldn't export mutation JSON."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ConvertAdminMutationPortableRelations(
|
||||||
|
Relations
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
Relations === undefined ||
|
||||||
|
Relations === null
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
PlantsUsed: [],
|
||||||
|
PlantsCreated: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsAdminPlainObject(Relations)) {
|
||||||
|
throw new Error(
|
||||||
|
"Relations must be an object."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Result = {
|
||||||
|
PlantsUsed: [],
|
||||||
|
PlantsCreated: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (
|
||||||
|
const RelationName
|
||||||
|
of [
|
||||||
|
"PlantsUsed",
|
||||||
|
"PlantsCreated"
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
const Values =
|
||||||
|
Relations[RelationName] ?? [];
|
||||||
|
|
||||||
|
if (!Array.isArray(Values)) {
|
||||||
|
throw new Error(
|
||||||
|
RelationName +
|
||||||
|
" must be an array of Plant Keys."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Value of Values) {
|
||||||
|
if (typeof Value !== "string") {
|
||||||
|
throw new Error(
|
||||||
|
RelationName +
|
||||||
|
" must use Plant Keys, not numeric IDs."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Plant =
|
||||||
|
AdminMutationPlantCatalogue[
|
||||||
|
Value
|
||||||
|
];
|
||||||
|
|
||||||
|
if (Plant === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
RelationName +
|
||||||
|
" references unknown Plant Key: " +
|
||||||
|
Value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result[RelationName].push(
|
||||||
|
Number(Plant.Id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result[RelationName] = [
|
||||||
|
...new Set(
|
||||||
|
Result[RelationName]
|
||||||
|
)
|
||||||
|
].sort(
|
||||||
|
(A, B) => A - B
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function ImportAdminMutationJson(
|
||||||
|
Event
|
||||||
|
) {
|
||||||
|
const Input = Event.target;
|
||||||
|
const File = Input.files?.[0];
|
||||||
|
|
||||||
|
Input.value = "";
|
||||||
|
|
||||||
|
if (File === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const DocumentData =
|
||||||
|
await ReadAdminJsonFile(
|
||||||
|
File
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
IsAdminPlainObject(
|
||||||
|
DocumentData
|
||||||
|
) &&
|
||||||
|
DocumentData.Type !==
|
||||||
|
undefined &&
|
||||||
|
DocumentData.Type !== "Mutation"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"That JSON file is not a mutation export."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
IsAdminPlainObject(
|
||||||
|
DocumentData
|
||||||
|
) &&
|
||||||
|
DocumentData.Version !==
|
||||||
|
undefined &&
|
||||||
|
Number(DocumentData.Version) !== 1
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Unsupported mutation JSON version."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Imported =
|
||||||
|
IsAdminPlainObject(
|
||||||
|
DocumentData?.Mutation
|
||||||
|
)
|
||||||
|
? DocumentData.Mutation
|
||||||
|
: DocumentData;
|
||||||
|
|
||||||
|
if (!IsAdminPlainObject(Imported)) {
|
||||||
|
throw new Error(
|
||||||
|
"Mutation JSON must contain a mutation object."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MutationKey =
|
||||||
|
String(
|
||||||
|
Imported.MutationKey ?? ""
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
const Existing =
|
||||||
|
AdminMutationCatalogue[
|
||||||
|
MutationKey
|
||||||
|
];
|
||||||
|
|
||||||
|
const Mutation = {
|
||||||
|
Id:
|
||||||
|
Existing?.Id ??
|
||||||
|
GetNextAdminMutationId(),
|
||||||
|
MutationKey: MutationKey,
|
||||||
|
Name:
|
||||||
|
String(
|
||||||
|
Imported.Name ?? ""
|
||||||
|
).trim(),
|
||||||
|
Description:
|
||||||
|
typeof Imported.Description ===
|
||||||
|
"string"
|
||||||
|
? Imported.Description.trim()
|
||||||
|
: "",
|
||||||
|
Hint:
|
||||||
|
typeof Imported.Hint ===
|
||||||
|
"string"
|
||||||
|
? Imported.Hint.trim()
|
||||||
|
: "",
|
||||||
|
Priority:
|
||||||
|
Number(
|
||||||
|
Imported.Priority ?? 0
|
||||||
|
),
|
||||||
|
Chance:
|
||||||
|
Number(
|
||||||
|
Imported.Chance
|
||||||
|
),
|
||||||
|
Cooldown:
|
||||||
|
Number(
|
||||||
|
Imported.Cooldown
|
||||||
|
),
|
||||||
|
Rotation:
|
||||||
|
Imported.Rotation ??
|
||||||
|
"None",
|
||||||
|
AllowImmature:
|
||||||
|
Imported.AllowImmature ===
|
||||||
|
true,
|
||||||
|
Pattern:
|
||||||
|
CloneAdminMutationValue(
|
||||||
|
Imported.Pattern
|
||||||
|
),
|
||||||
|
Success:
|
||||||
|
CloneAdminMutationValue(
|
||||||
|
Imported.Success
|
||||||
|
),
|
||||||
|
Failure:
|
||||||
|
CloneAdminMutationValue(
|
||||||
|
Imported.Failure ??
|
||||||
|
"Keep"
|
||||||
|
),
|
||||||
|
Relations:
|
||||||
|
ConvertAdminMutationPortableRelations(
|
||||||
|
Imported.Relations
|
||||||
|
),
|
||||||
|
IsNew:
|
||||||
|
Existing === undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
ValidateAdminMutation(
|
||||||
|
Mutation,
|
||||||
|
AdminMutationCatalogue,
|
||||||
|
AdminMutationPlantCatalogue,
|
||||||
|
Existing === undefined
|
||||||
|
? null
|
||||||
|
: MutationKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Existing === undefined) {
|
||||||
|
StartNewAdminMutation();
|
||||||
|
AdminMutationEditingKey = null;
|
||||||
|
} else {
|
||||||
|
LoadAdminMutationIntoForm(
|
||||||
|
MutationKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetAdminMutationField(
|
||||||
|
"AdminMutationId",
|
||||||
|
Mutation.Id
|
||||||
|
);
|
||||||
|
SetAdminMutationField(
|
||||||
|
"AdminMutationKey",
|
||||||
|
Mutation.MutationKey
|
||||||
|
);
|
||||||
|
SetAdminMutationField(
|
||||||
|
"AdminMutationName",
|
||||||
|
Mutation.Name
|
||||||
|
);
|
||||||
|
SetAdminMutationField(
|
||||||
|
"AdminMutationDescription",
|
||||||
|
Mutation.Description
|
||||||
|
);
|
||||||
|
SetAdminMutationField(
|
||||||
|
"AdminMutationHint",
|
||||||
|
Mutation.Hint
|
||||||
|
);
|
||||||
|
SetAdminMutationField(
|
||||||
|
"AdminMutationPriority",
|
||||||
|
Mutation.Priority
|
||||||
|
);
|
||||||
|
SetAdminMutationField(
|
||||||
|
"AdminMutationChance",
|
||||||
|
Mutation.Chance * 100
|
||||||
|
);
|
||||||
|
SetAdminMutationDurationFields(
|
||||||
|
Mutation.Cooldown
|
||||||
|
);
|
||||||
|
SetAdminMutationField(
|
||||||
|
"AdminMutationRotation",
|
||||||
|
Mutation.Rotation
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMutationAllowImmature"
|
||||||
|
).checked =
|
||||||
|
Mutation.AllowImmature;
|
||||||
|
|
||||||
|
AdminMutationPattern =
|
||||||
|
CloneAdminMutationMatrix(
|
||||||
|
Mutation.Pattern,
|
||||||
|
"Any"
|
||||||
|
);
|
||||||
|
AdminMutationResult =
|
||||||
|
NormalizeAdminMutationMatrix(
|
||||||
|
Mutation.Success,
|
||||||
|
AdminMutationPattern[0]
|
||||||
|
.length,
|
||||||
|
AdminMutationPattern.length,
|
||||||
|
"Keep"
|
||||||
|
);
|
||||||
|
|
||||||
|
SetAdminMutationField(
|
||||||
|
"AdminMutationWidth",
|
||||||
|
AdminMutationPattern[0]
|
||||||
|
.length
|
||||||
|
);
|
||||||
|
SetAdminMutationField(
|
||||||
|
"AdminMutationHeight",
|
||||||
|
AdminMutationPattern.length
|
||||||
|
);
|
||||||
|
|
||||||
|
LoadAdminMutationFailure(
|
||||||
|
Mutation.Failure
|
||||||
|
);
|
||||||
|
SetAdminMutationRelations(
|
||||||
|
Mutation.Relations
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMutationId"
|
||||||
|
).readOnly =
|
||||||
|
Existing !== undefined;
|
||||||
|
document.getElementById(
|
||||||
|
"AdminMutationKey"
|
||||||
|
).readOnly =
|
||||||
|
Existing !== undefined;
|
||||||
|
|
||||||
|
AdminMutationSelectedCell = null;
|
||||||
|
HideAdminMutationCellEditor();
|
||||||
|
RenderAdminMutationGrids();
|
||||||
|
UpdateAdminMutationFailureVisibility();
|
||||||
|
UpdateAdminMutationCooldownHint();
|
||||||
|
RenderAdminMutationJsonPreview();
|
||||||
|
|
||||||
|
SetAdminMutationMessage(
|
||||||
|
Existing === undefined
|
||||||
|
? "Mutation JSON imported as a new mutation. Review it, then save when ready."
|
||||||
|
: "Mutation JSON imported over " +
|
||||||
|
MutationKey +
|
||||||
|
". Review it, then save when ready."
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminMutationMessage(
|
||||||
|
Error.message ??
|
||||||
|
"Couldn't import mutation JSON."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function SetAdminMutationDurationFields(
|
function SetAdminMutationDurationFields(
|
||||||
Milliseconds
|
Milliseconds
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,485 @@
|
|||||||
|
let AdminOverviewPlants = {};
|
||||||
|
let AdminOverviewMutations = {};
|
||||||
|
|
||||||
|
|
||||||
|
async function AdminOverviewRequest(
|
||||||
|
Endpoint,
|
||||||
|
Action = "List"
|
||||||
|
) {
|
||||||
|
const SaveKey =
|
||||||
|
GetStoredAdminSaveKey();
|
||||||
|
|
||||||
|
if (SaveKey === null) {
|
||||||
|
throw new Error(
|
||||||
|
"A valid Account Key is required."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Response = await fetch(
|
||||||
|
AdminApiUrl + "/" + Endpoint,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type":
|
||||||
|
"application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
SaveKey: SaveKey,
|
||||||
|
Action: Action
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Result =
|
||||||
|
await Response.json();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Response.ok ||
|
||||||
|
Result.Success !== true
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Result.Error ??
|
||||||
|
"Admin overview request failed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminOverviewPlantKeyById(
|
||||||
|
PlantId
|
||||||
|
) {
|
||||||
|
return Object.entries(
|
||||||
|
AdminOverviewPlants
|
||||||
|
).find(
|
||||||
|
([, Plant]) =>
|
||||||
|
Number(Plant.Id) ===
|
||||||
|
Number(PlantId)
|
||||||
|
)?.[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminOverviewMissingSprites(
|
||||||
|
PlantKey,
|
||||||
|
Plant
|
||||||
|
) {
|
||||||
|
const StaticImages =
|
||||||
|
Array.isArray(
|
||||||
|
PlantImages?.[PlantKey]
|
||||||
|
)
|
||||||
|
? PlantImages[PlantKey]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const ApiStages =
|
||||||
|
new Set(
|
||||||
|
(
|
||||||
|
Array.isArray(
|
||||||
|
Plant.ImageStages
|
||||||
|
)
|
||||||
|
? Plant.ImageStages
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
Stage =>
|
||||||
|
Number(
|
||||||
|
Stage?.Stage ??
|
||||||
|
Stage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
Stage =>
|
||||||
|
Number.isInteger(Stage) &&
|
||||||
|
Stage > 0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const HighestApiStage =
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
...ApiStages
|
||||||
|
);
|
||||||
|
|
||||||
|
const HighestStage =
|
||||||
|
Math.max(
|
||||||
|
3,
|
||||||
|
StaticImages.length,
|
||||||
|
HighestApiStage
|
||||||
|
);
|
||||||
|
|
||||||
|
const Missing = [];
|
||||||
|
|
||||||
|
for (
|
||||||
|
let Stage = 1;
|
||||||
|
Stage <= HighestStage;
|
||||||
|
Stage++
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
ApiStages.has(Stage) ||
|
||||||
|
typeof StaticImages[
|
||||||
|
Stage - 1
|
||||||
|
] === "string"
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Missing.push(
|
||||||
|
Stage + ".png"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function CollectAdminOverviewCreatedPlants() {
|
||||||
|
const Created = new Set();
|
||||||
|
|
||||||
|
function AddResultValue(Value) {
|
||||||
|
if (typeof Value === "string") {
|
||||||
|
if (
|
||||||
|
Value !== "Keep" &&
|
||||||
|
Value !== "Empty" &&
|
||||||
|
!Value.startsWith("$") &&
|
||||||
|
AdminOverviewPlants[
|
||||||
|
Value
|
||||||
|
] !== undefined
|
||||||
|
) {
|
||||||
|
Created.add(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Value !== null &&
|
||||||
|
typeof Value === "object" &&
|
||||||
|
!Array.isArray(Value) &&
|
||||||
|
typeof Value.Plant === "string"
|
||||||
|
) {
|
||||||
|
AddResultValue(
|
||||||
|
Value.Plant
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
const Mutation
|
||||||
|
of Object.values(
|
||||||
|
AdminOverviewMutations
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (Mutation.Archived === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
const PlantId
|
||||||
|
of Mutation.Relations
|
||||||
|
?.PlantsCreated ?? []
|
||||||
|
) {
|
||||||
|
const PlantKey =
|
||||||
|
GetAdminOverviewPlantKeyById(
|
||||||
|
PlantId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (PlantKey !== null) {
|
||||||
|
Created.add(PlantKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
const Row
|
||||||
|
of Mutation.Success ?? []
|
||||||
|
) {
|
||||||
|
if (!Array.isArray(Row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Cell of Row) {
|
||||||
|
AddResultValue(Cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Created;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function CreateAdminOverviewIssueList(
|
||||||
|
Items,
|
||||||
|
NestedValues = null
|
||||||
|
) {
|
||||||
|
const List =
|
||||||
|
document.createElement("ul");
|
||||||
|
|
||||||
|
List.className =
|
||||||
|
"AdminOverviewList";
|
||||||
|
|
||||||
|
for (const Item of Items) {
|
||||||
|
const ListItem =
|
||||||
|
document.createElement("li");
|
||||||
|
|
||||||
|
const Label =
|
||||||
|
document.createElement("span");
|
||||||
|
|
||||||
|
Label.textContent = Item.Label;
|
||||||
|
ListItem.appendChild(Label);
|
||||||
|
|
||||||
|
const Nested =
|
||||||
|
NestedValues?.(Item) ?? [];
|
||||||
|
|
||||||
|
if (Nested.length > 0) {
|
||||||
|
const NestedList =
|
||||||
|
document.createElement("ul");
|
||||||
|
|
||||||
|
for (const Value of Nested) {
|
||||||
|
const NestedItem =
|
||||||
|
document.createElement("li");
|
||||||
|
|
||||||
|
NestedItem.textContent =
|
||||||
|
Value;
|
||||||
|
NestedList.appendChild(
|
||||||
|
NestedItem
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItem.appendChild(
|
||||||
|
NestedList
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List.appendChild(ListItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return List;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function RenderAdminOverviewSection(
|
||||||
|
ContainerId,
|
||||||
|
Title,
|
||||||
|
Items,
|
||||||
|
NestedValues = null
|
||||||
|
) {
|
||||||
|
const Container =
|
||||||
|
document.getElementById(
|
||||||
|
ContainerId
|
||||||
|
);
|
||||||
|
|
||||||
|
Container.replaceChildren();
|
||||||
|
|
||||||
|
const Heading =
|
||||||
|
document.createElement("h3");
|
||||||
|
|
||||||
|
Heading.textContent = Title;
|
||||||
|
Container.appendChild(Heading);
|
||||||
|
|
||||||
|
if (Items.length === 0) {
|
||||||
|
const Empty =
|
||||||
|
document.createElement("p");
|
||||||
|
|
||||||
|
Empty.className =
|
||||||
|
"AdminOverviewEmpty";
|
||||||
|
Empty.textContent = "None";
|
||||||
|
Container.appendChild(Empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Container.appendChild(
|
||||||
|
CreateAdminOverviewIssueList(
|
||||||
|
Items,
|
||||||
|
NestedValues
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function RenderAdminOverview() {
|
||||||
|
const CreatedPlants =
|
||||||
|
CollectAdminOverviewCreatedPlants();
|
||||||
|
|
||||||
|
const PlantEntries =
|
||||||
|
Object.entries(
|
||||||
|
AdminOverviewPlants
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
([, Plant]) =>
|
||||||
|
Plant.Archived !== true
|
||||||
|
)
|
||||||
|
.sort(
|
||||||
|
([KeyA, PlantA], [KeyB, PlantB]) =>
|
||||||
|
Number(PlantA.Id) -
|
||||||
|
Number(PlantB.Id) ||
|
||||||
|
KeyA.localeCompare(KeyB)
|
||||||
|
);
|
||||||
|
|
||||||
|
const MissingSprites = [];
|
||||||
|
const NoWaysToObtain = [];
|
||||||
|
|
||||||
|
for (
|
||||||
|
const [PlantKey, Plant]
|
||||||
|
of PlantEntries
|
||||||
|
) {
|
||||||
|
const Missing =
|
||||||
|
GetAdminOverviewMissingSprites(
|
||||||
|
PlantKey,
|
||||||
|
Plant
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Missing.length > 0) {
|
||||||
|
MissingSprites.push({
|
||||||
|
Label:
|
||||||
|
Plant.Name ?? PlantKey,
|
||||||
|
Missing: Missing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Plant.Shop?.ShopPlant !== true &&
|
||||||
|
!CreatedPlants.has(PlantKey)
|
||||||
|
) {
|
||||||
|
NoWaysToObtain.push({
|
||||||
|
Label:
|
||||||
|
Plant.Name ?? PlantKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MutationEntries =
|
||||||
|
Object.entries(
|
||||||
|
AdminOverviewMutations
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
([, Mutation]) =>
|
||||||
|
Mutation.Archived !== true
|
||||||
|
)
|
||||||
|
.sort(
|
||||||
|
([KeyA, MutationA], [KeyB, MutationB]) =>
|
||||||
|
Number(MutationA.Id) -
|
||||||
|
Number(MutationB.Id) ||
|
||||||
|
KeyA.localeCompare(KeyB)
|
||||||
|
);
|
||||||
|
|
||||||
|
const NoDescription = [];
|
||||||
|
const NoHint = [];
|
||||||
|
|
||||||
|
for (
|
||||||
|
const [MutationKey, Mutation]
|
||||||
|
of MutationEntries
|
||||||
|
) {
|
||||||
|
const Label =
|
||||||
|
Mutation.Name?.trim() ||
|
||||||
|
MutationKey;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof Mutation.Description !==
|
||||||
|
"string" ||
|
||||||
|
Mutation.Description.trim() === ""
|
||||||
|
) {
|
||||||
|
NoDescription.push({
|
||||||
|
Label: Label
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof Mutation.Hint !==
|
||||||
|
"string" ||
|
||||||
|
Mutation.Hint.trim() === ""
|
||||||
|
) {
|
||||||
|
NoHint.push({
|
||||||
|
Label: Label
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderAdminOverviewSection(
|
||||||
|
"AdminOverviewMissingSprites",
|
||||||
|
"Missing sprites",
|
||||||
|
MissingSprites,
|
||||||
|
Item => Item.Missing
|
||||||
|
);
|
||||||
|
|
||||||
|
RenderAdminOverviewSection(
|
||||||
|
"AdminOverviewNoObtain",
|
||||||
|
"No ways to obtain",
|
||||||
|
NoWaysToObtain
|
||||||
|
);
|
||||||
|
|
||||||
|
RenderAdminOverviewSection(
|
||||||
|
"AdminOverviewNoDescription",
|
||||||
|
"No description",
|
||||||
|
NoDescription
|
||||||
|
);
|
||||||
|
|
||||||
|
RenderAdminOverviewSection(
|
||||||
|
"AdminOverviewNoHint",
|
||||||
|
"No hint",
|
||||||
|
NoHint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function StartAdminOverview() {
|
||||||
|
const Overview =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminOverview"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Overview === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const Status =
|
||||||
|
await GetAdminStatus();
|
||||||
|
|
||||||
|
if (
|
||||||
|
Status.Success !== true ||
|
||||||
|
Status.IsAdmin !== true
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
PlantResult,
|
||||||
|
MutationResult
|
||||||
|
] = await Promise.all([
|
||||||
|
AdminOverviewRequest(
|
||||||
|
"AdminPlants.php"
|
||||||
|
),
|
||||||
|
AdminOverviewRequest(
|
||||||
|
"AdminMutations.php"
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
AdminOverviewPlants =
|
||||||
|
PlantResult.Plants ?? {};
|
||||||
|
AdminOverviewMutations =
|
||||||
|
MutationResult.Mutations ?? {};
|
||||||
|
|
||||||
|
RenderAdminOverview();
|
||||||
|
Overview.hidden = false;
|
||||||
|
} catch (Error) {
|
||||||
|
console.error(
|
||||||
|
"Couldn't load admin overview:",
|
||||||
|
Error
|
||||||
|
);
|
||||||
|
|
||||||
|
const Message =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminOverviewMessage"
|
||||||
|
);
|
||||||
|
|
||||||
|
Message.textContent =
|
||||||
|
Error.message ??
|
||||||
|
"Couldn't load admin overview.";
|
||||||
|
Overview.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"DOMContentLoaded",
|
||||||
|
StartAdminOverview
|
||||||
|
);
|
||||||
@@ -71,6 +71,31 @@ function BindAdminPlantEditor() {
|
|||||||
DuplicateAdminPlant
|
DuplicateAdminPlant
|
||||||
);
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminPlantImportButton"
|
||||||
|
).addEventListener(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
|
document.getElementById(
|
||||||
|
"AdminPlantImportFile"
|
||||||
|
).click();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminPlantImportFile"
|
||||||
|
).addEventListener(
|
||||||
|
"change",
|
||||||
|
ImportAdminPlantJson
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminPlantExportButton"
|
||||||
|
).addEventListener(
|
||||||
|
"click",
|
||||||
|
ExportAdminPlantJson
|
||||||
|
);
|
||||||
|
|
||||||
document.getElementById(
|
document.getElementById(
|
||||||
"AdminPlantForm"
|
"AdminPlantForm"
|
||||||
).addEventListener(
|
).addEventListener(
|
||||||
@@ -930,6 +955,20 @@ async function SaveAdminPlant(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ValidateAdminPlant(
|
||||||
|
Plant,
|
||||||
|
AdminPlantCatalogue,
|
||||||
|
AdminPlantEditingKey
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminPlantMessage(
|
||||||
|
Error.message
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AdminPlantSavePending = true;
|
AdminPlantSavePending = true;
|
||||||
|
|
||||||
const SaveButton =
|
const SaveButton =
|
||||||
@@ -1833,6 +1872,297 @@ async function DeleteAdminPlantImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminPlantPortableData(
|
||||||
|
Plant
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
PlantKey: Plant.PlantKey,
|
||||||
|
Name: Plant.Name,
|
||||||
|
Description: Plant.Description,
|
||||||
|
Tags: [...Plant.Tags],
|
||||||
|
GrowthTime: Plant.GrowthTime,
|
||||||
|
HarvestMultiplier:
|
||||||
|
Plant.HarvestMultiplier,
|
||||||
|
Effects: JSON.parse(
|
||||||
|
JSON.stringify(
|
||||||
|
Plant.Effects ?? {}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Shop: {
|
||||||
|
ShopPlant:
|
||||||
|
Plant.ShopPlant === true,
|
||||||
|
BaseCost:
|
||||||
|
Plant.ShopPlant === true
|
||||||
|
? Plant.BaseCost
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ExportAdminPlantJson() {
|
||||||
|
const Form =
|
||||||
|
document.getElementById(
|
||||||
|
"AdminPlantForm"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!Form.reportValidity()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const Plant =
|
||||||
|
GetAdminPlantFormData();
|
||||||
|
|
||||||
|
ValidateAdminPlant(
|
||||||
|
Plant,
|
||||||
|
AdminPlantCatalogue,
|
||||||
|
AdminPlantEditingKey
|
||||||
|
);
|
||||||
|
|
||||||
|
DownloadAdminJson(
|
||||||
|
"Plant-" +
|
||||||
|
Plant.PlantKey +
|
||||||
|
".json",
|
||||||
|
{
|
||||||
|
Type: "Plant",
|
||||||
|
Version: 1,
|
||||||
|
Plant:
|
||||||
|
GetAdminPlantPortableData(
|
||||||
|
Plant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
SetAdminPlantMessage(
|
||||||
|
"Plant JSON exported. Sprites and numeric ID are not included."
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminPlantMessage(
|
||||||
|
Error.message ??
|
||||||
|
"Couldn't export plant JSON."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function ImportAdminPlantJson(
|
||||||
|
Event
|
||||||
|
) {
|
||||||
|
const Input = Event.target;
|
||||||
|
const File = Input.files?.[0];
|
||||||
|
|
||||||
|
Input.value = "";
|
||||||
|
|
||||||
|
if (File === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const DocumentData =
|
||||||
|
await ReadAdminJsonFile(
|
||||||
|
File
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
IsAdminPlainObject(
|
||||||
|
DocumentData
|
||||||
|
) &&
|
||||||
|
DocumentData.Type !==
|
||||||
|
undefined &&
|
||||||
|
DocumentData.Type !== "Plant"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"That JSON file is not a plant export."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
IsAdminPlainObject(
|
||||||
|
DocumentData
|
||||||
|
) &&
|
||||||
|
DocumentData.Version !==
|
||||||
|
undefined &&
|
||||||
|
Number(DocumentData.Version) !== 1
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Unsupported plant JSON version."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Imported =
|
||||||
|
IsAdminPlainObject(
|
||||||
|
DocumentData?.Plant
|
||||||
|
)
|
||||||
|
? DocumentData.Plant
|
||||||
|
: DocumentData;
|
||||||
|
|
||||||
|
if (!IsAdminPlainObject(Imported)) {
|
||||||
|
throw new Error(
|
||||||
|
"Plant JSON must contain a plant object."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlantKey =
|
||||||
|
String(
|
||||||
|
Imported.PlantKey ?? ""
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
const Existing =
|
||||||
|
AdminPlantCatalogue[
|
||||||
|
PlantKey
|
||||||
|
];
|
||||||
|
|
||||||
|
const Shop =
|
||||||
|
IsAdminPlainObject(
|
||||||
|
Imported.Shop
|
||||||
|
)
|
||||||
|
? Imported.Shop
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const Plant = {
|
||||||
|
Id:
|
||||||
|
Existing?.Id ??
|
||||||
|
GetNextAdminPlantId(),
|
||||||
|
PlantKey: PlantKey,
|
||||||
|
Name:
|
||||||
|
String(
|
||||||
|
Imported.Name ?? ""
|
||||||
|
).trim(),
|
||||||
|
Description:
|
||||||
|
typeof Imported.Description ===
|
||||||
|
"string"
|
||||||
|
? Imported.Description.trim()
|
||||||
|
: "",
|
||||||
|
Tags:
|
||||||
|
Array.isArray(Imported.Tags)
|
||||||
|
? Imported.Tags.map(
|
||||||
|
Tag =>
|
||||||
|
String(Tag).trim()
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
GrowthTime:
|
||||||
|
Number(
|
||||||
|
Imported.GrowthTime
|
||||||
|
),
|
||||||
|
HarvestMultiplier:
|
||||||
|
Number(
|
||||||
|
Imported.HarvestMultiplier ??
|
||||||
|
1.5
|
||||||
|
),
|
||||||
|
Effects:
|
||||||
|
IsAdminPlainObject(
|
||||||
|
Imported.Effects
|
||||||
|
)
|
||||||
|
? JSON.parse(
|
||||||
|
JSON.stringify(
|
||||||
|
Imported.Effects
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: {},
|
||||||
|
ShopPlant:
|
||||||
|
Shop.ShopPlant === true ||
|
||||||
|
Imported.ShopPlant === true,
|
||||||
|
BaseCost:
|
||||||
|
Shop.BaseCost ??
|
||||||
|
Imported.BaseCost ??
|
||||||
|
null,
|
||||||
|
IsNew:
|
||||||
|
Existing === undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
ValidateAdminPlant(
|
||||||
|
Plant,
|
||||||
|
AdminPlantCatalogue,
|
||||||
|
Existing === undefined
|
||||||
|
? null
|
||||||
|
: PlantKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Existing === undefined) {
|
||||||
|
StartNewAdminPlant();
|
||||||
|
AdminPlantEditingKey = null;
|
||||||
|
} else {
|
||||||
|
LoadAdminPlantIntoForm(
|
||||||
|
PlantKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetAdminPlantField(
|
||||||
|
"AdminPlantId",
|
||||||
|
Plant.Id
|
||||||
|
);
|
||||||
|
SetAdminPlantField(
|
||||||
|
"AdminPlantKey",
|
||||||
|
Plant.PlantKey
|
||||||
|
);
|
||||||
|
SetAdminPlantField(
|
||||||
|
"AdminPlantName",
|
||||||
|
Plant.Name
|
||||||
|
);
|
||||||
|
SetAdminPlantField(
|
||||||
|
"AdminPlantDescription",
|
||||||
|
Plant.Description
|
||||||
|
);
|
||||||
|
SetAdminPlantField(
|
||||||
|
"AdminPlantTags",
|
||||||
|
Plant.Tags.join(", ")
|
||||||
|
);
|
||||||
|
SetAdminPlantDurationFields(
|
||||||
|
Plant.GrowthTime
|
||||||
|
);
|
||||||
|
SetAdminPlantField(
|
||||||
|
"AdminPlantHarvestMultiplier",
|
||||||
|
Plant.HarvestMultiplier
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminPlantShopPlant"
|
||||||
|
).checked = Plant.ShopPlant;
|
||||||
|
|
||||||
|
SetAdminPlantField(
|
||||||
|
"AdminPlantBaseCost",
|
||||||
|
Plant.BaseCost ?? ""
|
||||||
|
);
|
||||||
|
SetAdminPlantField(
|
||||||
|
"AdminPlantEffects",
|
||||||
|
JSON.stringify(
|
||||||
|
Plant.Effects,
|
||||||
|
null,
|
||||||
|
4
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminPlantId"
|
||||||
|
).readOnly =
|
||||||
|
Existing !== undefined;
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
"AdminPlantKey"
|
||||||
|
).readOnly =
|
||||||
|
Existing !== undefined;
|
||||||
|
|
||||||
|
UpdateAdminPlantShopFields();
|
||||||
|
RenderAdminPlantPreview();
|
||||||
|
RenderAdminPlantImageManager();
|
||||||
|
|
||||||
|
SetAdminPlantMessage(
|
||||||
|
Existing === undefined
|
||||||
|
? "Plant JSON imported as a new plant. Review it, then save when ready."
|
||||||
|
: "Plant JSON imported over " +
|
||||||
|
PlantKey +
|
||||||
|
". Review it, then save when ready."
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
SetAdminPlantMessage(
|
||||||
|
Error.message ??
|
||||||
|
"Couldn't import plant JSON."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function FormatAdminDuration(
|
function FormatAdminDuration(
|
||||||
Milliseconds
|
Milliseconds
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,820 @@
|
|||||||
|
function IsAdminPlainObject(Value) {
|
||||||
|
return (
|
||||||
|
Value !== null &&
|
||||||
|
typeof Value === "object" &&
|
||||||
|
!Array.isArray(Value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ValidateAdminInteger(
|
||||||
|
Value,
|
||||||
|
Name,
|
||||||
|
Minimum = null
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!Number.isInteger(Value) ||
|
||||||
|
(
|
||||||
|
Minimum !== null &&
|
||||||
|
Value < Minimum
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Name + " must be " +
|
||||||
|
(
|
||||||
|
Minimum === null
|
||||||
|
? "an integer."
|
||||||
|
: "an integer of at least " +
|
||||||
|
Minimum + "."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ValidateAdminKey(
|
||||||
|
Value,
|
||||||
|
Name,
|
||||||
|
MaximumLength = 64
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
typeof Value !== "string" ||
|
||||||
|
Value.length === 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Name + " cannot be empty."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Value.length > MaximumLength ||
|
||||||
|
!/^[A-Za-z0-9_-]+$/.test(Value)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Name +
|
||||||
|
" may only contain letters, numbers, underscores and hyphens."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ValidateAdminPlant(
|
||||||
|
Plant,
|
||||||
|
Catalogue,
|
||||||
|
EditingKey = null
|
||||||
|
) {
|
||||||
|
if (!IsAdminPlainObject(Plant)) {
|
||||||
|
throw new Error(
|
||||||
|
"Plant data is invalid."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateAdminInteger(
|
||||||
|
Number(Plant.Id),
|
||||||
|
"ID",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const PlantKey =
|
||||||
|
String(
|
||||||
|
Plant.PlantKey ?? ""
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
ValidateAdminKey(
|
||||||
|
PlantKey,
|
||||||
|
"Plant Key"
|
||||||
|
);
|
||||||
|
|
||||||
|
const ExistingAtId =
|
||||||
|
Object.entries(
|
||||||
|
Catalogue ?? {}
|
||||||
|
).find(
|
||||||
|
([, Candidate]) =>
|
||||||
|
Number(Candidate?.Id) ===
|
||||||
|
Number(Plant.Id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
ExistingAtId !== undefined &&
|
||||||
|
ExistingAtId[0] !== PlantKey
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"ID " + Plant.Id +
|
||||||
|
" already belongs to " +
|
||||||
|
ExistingAtId[0] + "."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
EditingKey !== null &&
|
||||||
|
EditingKey !== PlantKey
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"An existing Plant Key cannot be changed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
EditingKey !== null &&
|
||||||
|
Number(
|
||||||
|
Catalogue?.[EditingKey]?.Id
|
||||||
|
) !== Number(Plant.Id)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"An existing plant ID cannot be changed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
EditingKey === null &&
|
||||||
|
Catalogue?.[PlantKey] !== undefined
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Plant Key " + PlantKey +
|
||||||
|
" already exists."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Name =
|
||||||
|
String(
|
||||||
|
Plant.Name ?? ""
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
if (
|
||||||
|
Name.length === 0 ||
|
||||||
|
Name.length > 64
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Name must contain between 1 and 64 characters."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof Plant.Description !==
|
||||||
|
"string"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Description must be text."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(Plant.Tags)) {
|
||||||
|
throw new Error(
|
||||||
|
"Tags must be an array."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Tag of Plant.Tags) {
|
||||||
|
if (
|
||||||
|
typeof Tag !== "string" ||
|
||||||
|
Tag.trim().length === 0 ||
|
||||||
|
Tag.trim().length > 64
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Tags contains an invalid tag."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateAdminInteger(
|
||||||
|
Number(Plant.GrowthTime),
|
||||||
|
"Growth time",
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const HarvestMultiplier =
|
||||||
|
Number(
|
||||||
|
Plant.HarvestMultiplier
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isFinite(
|
||||||
|
HarvestMultiplier
|
||||||
|
) ||
|
||||||
|
HarvestMultiplier < 0 ||
|
||||||
|
HarvestMultiplier > 9999.9999
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Harvest multiplier must be between 0 and 9999.9999."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsAdminPlainObject(Plant.Effects)) {
|
||||||
|
throw new Error(
|
||||||
|
"Effects JSON must be an object."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Plant.ShopPlant === true) {
|
||||||
|
ValidateAdminInteger(
|
||||||
|
Number(Plant.BaseCost),
|
||||||
|
"Base seed cost",
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GetAdminPlantKeyById(
|
||||||
|
PlantId,
|
||||||
|
PlantCatalogue
|
||||||
|
) {
|
||||||
|
return Object.entries(
|
||||||
|
PlantCatalogue ?? {}
|
||||||
|
).find(
|
||||||
|
([, Plant]) =>
|
||||||
|
Number(Plant?.Id) ===
|
||||||
|
Number(PlantId)
|
||||||
|
)?.[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ValidateAdminMutationMatrix(
|
||||||
|
Matrix,
|
||||||
|
Name,
|
||||||
|
Width = null,
|
||||||
|
Height = null
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!Array.isArray(Matrix) ||
|
||||||
|
Matrix.length < 1 ||
|
||||||
|
Matrix.length > 7
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Name +
|
||||||
|
" must contain between 1 and 7 rows."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MatrixWidth =
|
||||||
|
Array.isArray(Matrix[0])
|
||||||
|
? Matrix[0].length
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (
|
||||||
|
MatrixWidth < 1 ||
|
||||||
|
MatrixWidth > 7
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Name +
|
||||||
|
" rows must contain between 1 and 7 cells."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Row of Matrix) {
|
||||||
|
if (
|
||||||
|
!Array.isArray(Row) ||
|
||||||
|
Row.length !== MatrixWidth
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Name +
|
||||||
|
" must be rectangular."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Width !== null &&
|
||||||
|
MatrixWidth !== Width
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Name +
|
||||||
|
" width must match the pattern."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Height !== null &&
|
||||||
|
Matrix.length !== Height
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Name +
|
||||||
|
" height must match the pattern."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Width: MatrixWidth,
|
||||||
|
Height: Matrix.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ValidateAdminMutationTagList(
|
||||||
|
Value,
|
||||||
|
Name
|
||||||
|
) {
|
||||||
|
if (!Array.isArray(Value)) {
|
||||||
|
throw new Error(
|
||||||
|
Name + " must be an array."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Tag of Value) {
|
||||||
|
if (
|
||||||
|
typeof Tag !== "string" ||
|
||||||
|
Tag.trim().length === 0 ||
|
||||||
|
Tag.trim().length > 64
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Name +
|
||||||
|
" contains an invalid tag."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ValidateAdminCaptureName(
|
||||||
|
Capture
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
typeof Capture !== "string" ||
|
||||||
|
!/^[A-Za-z][A-Za-z0-9_]*$/.test(
|
||||||
|
Capture
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Capture names must start with a letter and contain only letters, numbers and underscores."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ValidateAdminMutationPatternCell(
|
||||||
|
Cell,
|
||||||
|
PlantCatalogue,
|
||||||
|
Captures
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
Cell === null ||
|
||||||
|
Cell === "Any" ||
|
||||||
|
Cell === "Empty"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Cell === "string") {
|
||||||
|
if (
|
||||||
|
PlantCatalogue?.[Cell] ===
|
||||||
|
undefined
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Pattern references unknown plant: " +
|
||||||
|
Cell
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsAdminPlainObject(Cell)) {
|
||||||
|
throw new Error(
|
||||||
|
"Pattern contains an invalid matcher."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Cell.Plant !== undefined &&
|
||||||
|
(
|
||||||
|
typeof Cell.Plant !==
|
||||||
|
"string" ||
|
||||||
|
PlantCatalogue?.[
|
||||||
|
Cell.Plant
|
||||||
|
] === undefined
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Pattern matcher references an unknown plant."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
const TagField
|
||||||
|
of [
|
||||||
|
"Tags",
|
||||||
|
"TagsAny",
|
||||||
|
"TagsNot"
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
if (Cell[TagField] !== undefined) {
|
||||||
|
ValidateAdminMutationTagList(
|
||||||
|
Cell[TagField],
|
||||||
|
TagField
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Cell.Capture !== undefined) {
|
||||||
|
ValidateAdminCaptureName(
|
||||||
|
Cell.Capture
|
||||||
|
);
|
||||||
|
|
||||||
|
Captures.add(
|
||||||
|
Cell.Capture
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ValidateAdminMutationResultCell(
|
||||||
|
Cell,
|
||||||
|
PlantCatalogue,
|
||||||
|
Captures
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
Cell === null ||
|
||||||
|
Cell === "Keep" ||
|
||||||
|
Cell === "Empty"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let PlantValue = null;
|
||||||
|
|
||||||
|
if (typeof Cell === "string") {
|
||||||
|
PlantValue = Cell;
|
||||||
|
} else if (
|
||||||
|
IsAdminPlainObject(Cell) &&
|
||||||
|
typeof Cell.Plant === "string"
|
||||||
|
) {
|
||||||
|
PlantValue = Cell.Plant;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlantValue === null) {
|
||||||
|
throw new Error(
|
||||||
|
"Result contains an invalid value."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlantValue.startsWith("$")) {
|
||||||
|
const Capture =
|
||||||
|
PlantValue.slice(1);
|
||||||
|
|
||||||
|
if (!Captures.has(Capture)) {
|
||||||
|
throw new Error(
|
||||||
|
"Result references unknown capture: " +
|
||||||
|
Capture
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
PlantCatalogue?.[
|
||||||
|
PlantValue
|
||||||
|
] === undefined
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Result references unknown plant: " +
|
||||||
|
PlantValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ValidateAdminMutationRelations(
|
||||||
|
Relations,
|
||||||
|
PlantCatalogue
|
||||||
|
) {
|
||||||
|
if (!IsAdminPlainObject(Relations)) {
|
||||||
|
throw new Error(
|
||||||
|
"Relations must be an object."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ValidIds =
|
||||||
|
new Set(
|
||||||
|
Object.values(
|
||||||
|
PlantCatalogue ?? {}
|
||||||
|
).map(
|
||||||
|
Plant =>
|
||||||
|
Number(Plant.Id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (
|
||||||
|
const Name
|
||||||
|
of [
|
||||||
|
"PlantsUsed",
|
||||||
|
"PlantsCreated"
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
const Values =
|
||||||
|
Relations[Name] ?? [];
|
||||||
|
|
||||||
|
if (!Array.isArray(Values)) {
|
||||||
|
throw new Error(
|
||||||
|
Name + " must be an array."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const PlantId of Values) {
|
||||||
|
if (
|
||||||
|
!Number.isInteger(
|
||||||
|
Number(PlantId)
|
||||||
|
) ||
|
||||||
|
!ValidIds.has(
|
||||||
|
Number(PlantId)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
Name +
|
||||||
|
" references a plant that does not exist."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ValidateAdminMutation(
|
||||||
|
Mutation,
|
||||||
|
MutationCatalogue,
|
||||||
|
PlantCatalogue,
|
||||||
|
EditingKey = null
|
||||||
|
) {
|
||||||
|
if (!IsAdminPlainObject(Mutation)) {
|
||||||
|
throw new Error(
|
||||||
|
"Mutation data is invalid."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateAdminInteger(
|
||||||
|
Number(Mutation.Id),
|
||||||
|
"ID",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const MutationKey =
|
||||||
|
String(
|
||||||
|
Mutation.MutationKey ?? ""
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
ValidateAdminKey(
|
||||||
|
MutationKey,
|
||||||
|
"Mutation Key"
|
||||||
|
);
|
||||||
|
|
||||||
|
const ExistingAtId =
|
||||||
|
Object.entries(
|
||||||
|
MutationCatalogue ?? {}
|
||||||
|
).find(
|
||||||
|
([, Candidate]) =>
|
||||||
|
Number(Candidate?.Id) ===
|
||||||
|
Number(Mutation.Id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
ExistingAtId !== undefined &&
|
||||||
|
ExistingAtId[0] !== MutationKey
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"ID " + Mutation.Id +
|
||||||
|
" already belongs to " +
|
||||||
|
ExistingAtId[0] + "."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
EditingKey !== null &&
|
||||||
|
EditingKey !== MutationKey
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"An existing Mutation Key cannot be changed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
EditingKey !== null &&
|
||||||
|
Number(
|
||||||
|
MutationCatalogue
|
||||||
|
?.[EditingKey]?.Id
|
||||||
|
) !== Number(Mutation.Id)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"An existing mutation ID cannot be changed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
EditingKey === null &&
|
||||||
|
MutationCatalogue?.[
|
||||||
|
MutationKey
|
||||||
|
] !== undefined
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Mutation Key " +
|
||||||
|
MutationKey +
|
||||||
|
" already exists."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Name =
|
||||||
|
String(
|
||||||
|
Mutation.Name ?? ""
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
if (
|
||||||
|
Name.length === 0 ||
|
||||||
|
Name.length > 128
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Name must contain between 1 and 128 characters."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof Mutation.Description !==
|
||||||
|
"string"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Description must be text."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Mutation.Hint !== null &&
|
||||||
|
Mutation.Hint !== undefined &&
|
||||||
|
typeof Mutation.Hint !== "string"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Hint must be text."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateAdminInteger(
|
||||||
|
Number(Mutation.Priority),
|
||||||
|
"Priority"
|
||||||
|
);
|
||||||
|
|
||||||
|
const Chance =
|
||||||
|
Number(
|
||||||
|
Mutation.Chance
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isFinite(Chance) ||
|
||||||
|
Chance < 0 ||
|
||||||
|
Chance > 1
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Chance must be between 0 and 100%."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateAdminInteger(
|
||||||
|
Number(Mutation.Cooldown),
|
||||||
|
"Cooldown",
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
Mutation.Rotation !== "None" &&
|
||||||
|
Mutation.Rotation !== "Any"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Rotation must be None or Any."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PatternSize =
|
||||||
|
ValidateAdminMutationMatrix(
|
||||||
|
Mutation.Pattern,
|
||||||
|
"Pattern"
|
||||||
|
);
|
||||||
|
|
||||||
|
const Captures =
|
||||||
|
new Set();
|
||||||
|
|
||||||
|
for (const Row of Mutation.Pattern) {
|
||||||
|
for (const Cell of Row) {
|
||||||
|
ValidateAdminMutationPatternCell(
|
||||||
|
Cell,
|
||||||
|
PlantCatalogue,
|
||||||
|
Captures
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateAdminMutationMatrix(
|
||||||
|
Mutation.Success,
|
||||||
|
"Success result",
|
||||||
|
PatternSize.Width,
|
||||||
|
PatternSize.Height
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const Row of Mutation.Success) {
|
||||||
|
for (const Cell of Row) {
|
||||||
|
ValidateAdminMutationResultCell(
|
||||||
|
Cell,
|
||||||
|
PlantCatalogue,
|
||||||
|
Captures
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Mutation.Failure !== "Keep" &&
|
||||||
|
Mutation.Failure !== "Clear"
|
||||||
|
) {
|
||||||
|
ValidateAdminMutationMatrix(
|
||||||
|
Mutation.Failure,
|
||||||
|
"Failure result",
|
||||||
|
PatternSize.Width,
|
||||||
|
PatternSize.Height
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const Row of Mutation.Failure) {
|
||||||
|
for (const Cell of Row) {
|
||||||
|
ValidateAdminMutationResultCell(
|
||||||
|
Cell,
|
||||||
|
PlantCatalogue,
|
||||||
|
Captures
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateAdminMutationRelations(
|
||||||
|
Mutation.Relations ?? {},
|
||||||
|
PlantCatalogue
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function DownloadAdminJson(
|
||||||
|
FileName,
|
||||||
|
Data
|
||||||
|
) {
|
||||||
|
const BlobData =
|
||||||
|
new Blob(
|
||||||
|
[
|
||||||
|
JSON.stringify(
|
||||||
|
Data,
|
||||||
|
null,
|
||||||
|
4
|
||||||
|
) + "\n"
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: "application/json"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Url =
|
||||||
|
URL.createObjectURL(
|
||||||
|
BlobData
|
||||||
|
);
|
||||||
|
|
||||||
|
const Link =
|
||||||
|
document.createElement("a");
|
||||||
|
|
||||||
|
Link.href = Url;
|
||||||
|
Link.download = FileName;
|
||||||
|
Link.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ReadAdminJsonFile(File) {
|
||||||
|
return new Promise(
|
||||||
|
(Resolve, Reject) => {
|
||||||
|
const Reader =
|
||||||
|
new FileReader();
|
||||||
|
|
||||||
|
Reader.onload = () => {
|
||||||
|
try {
|
||||||
|
Resolve(
|
||||||
|
JSON.parse(
|
||||||
|
String(
|
||||||
|
Reader.result ?? ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (Error) {
|
||||||
|
Reject(
|
||||||
|
new Error(
|
||||||
|
"The selected file is not valid JSON."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Reader.onerror = () => {
|
||||||
|
Reject(
|
||||||
|
new Error(
|
||||||
|
"Couldn't read the selected JSON file."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Reader.readAsText(File);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2343,3 +2343,88 @@ input, button, textarea, select {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.AdminEditorTransferActions {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminOverview,
|
||||||
|
.AdminMassSelectionPanel,
|
||||||
|
.AdminMassEditorForm {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminOverviewGroups {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminOverviewGroups > section > h2 {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminOverviewGroups h3 {
|
||||||
|
margin: 1rem 0 0.35rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminOverviewGroups h3:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminOverviewList,
|
||||||
|
.AdminOverviewList ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminOverviewList ul {
|
||||||
|
color: var(--Subtext1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminOverviewEmpty {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--Overlay2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminOverview > .PageMessage:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminMassSelectionBody {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminMassChecklist {
|
||||||
|
max-height: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminMassField {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminMassApplyField {
|
||||||
|
align-self: start;
|
||||||
|
margin-top: 1.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminMassField :disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.AdminOverviewGroups,
|
||||||
|
.AdminMassField {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AdminMassApplyField {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user