diff --git a/Pages/Admin.php b/Pages/Admin.php index d93fc3f..bbd2c21 100644 --- a/Pages/Admin.php +++ b/Pages/Admin.php @@ -30,9 +30,31 @@ $PageSection = "Admin"; hidden >

- Admin access verified. Garden content - editors will live here. + Content tools for editing the Garden catalogue. + Changes here affect the live game data.

+ +
+ + Plant editor + + Add plants and edit their catalogue data. + + + + + Mutation editor + + Build mutation recipes with a visual grid. + + +
diff --git a/Pages/AdminMutations.php b/Pages/AdminMutations.php new file mode 100644 index 0000000..81a9700 --- /dev/null +++ b/Pages/AdminMutations.php @@ -0,0 +1,432 @@ + + + + + + + + + + +
+

Mutation editor

+ +

+ Edit mutation settings and build the recipe visually. + Select a cell in either grid to change what it means. +

+ +

+ Checking admin access... +

+ + +
+ + + + + + + + diff --git a/Pages/AdminPlants.php b/Pages/AdminPlants.php new file mode 100644 index 0000000..14d5662 --- /dev/null +++ b/Pages/AdminPlants.php @@ -0,0 +1,225 @@ + + + + + + + + + + +
+

Plant editor

+ +

+ Add plants or edit existing catalogue data. Numeric IDs + and existing Plant Keys stay permanent once created. +

+ +

+ Checking admin access... +

+ + +
+ + + + + + + diff --git a/Scripts/Garden/AdminMutationEditor.js b/Scripts/Garden/AdminMutationEditor.js new file mode 100644 index 0000000..1dd2fb9 --- /dev/null +++ b/Scripts/Garden/AdminMutationEditor.js @@ -0,0 +1,2266 @@ +let AdminMutationCatalogue = {}; +let AdminMutationPlantCatalogue = {}; +let AdminMutationEditingKey = null; +let AdminMutationSavePending = false; + +let AdminMutationPattern = [ + ["Any", "Empty", "Any"] +]; + +let AdminMutationResult = [ + ["Keep", "Keep", "Keep"] +]; + +let AdminMutationSelectedCell = null; + + +async function StartAdminMutationEditor() { + try { + const Status = + await GetAdminStatus(); + + if ( + Status.Success !== true || + Status.IsAdmin !== true + ) { + SetAdminMutationMessage( + "Admin access required." + ); + + return; + } + + BindAdminMutationEditor(); + await LoadAdminMutationData(); + } catch (Error) { + console.error( + "Couldn't start Mutation editor:", + Error + ); + + SetAdminMutationMessage( + Error.message ?? + "Couldn't load the Mutation editor." + ); + } +} + + +function BindAdminMutationEditor() { + document.getElementById( + "AdminMutationSelect" + ).addEventListener( + "change", + Event => { + const MutationKey = + Event.target.value; + + if (MutationKey === "") { + StartNewAdminMutation(); + return; + } + + LoadAdminMutationIntoForm( + MutationKey + ); + } + ); + + document.getElementById( + "AdminMutationNewButton" + ).addEventListener( + "click", + StartNewAdminMutation + ); + + document.getElementById( + "AdminMutationResizeButton" + ).addEventListener( + "click", + ResizeAdminMutationRecipe + ); + + document.getElementById( + "AdminMutationForm" + ).addEventListener( + "submit", + SaveAdminMutation + ); + + document.getElementById( + "AdminMutationPatternCellType" + ).addEventListener( + "change", + UpdateAdminMutationPatternControlVisibility + ); + + document.getElementById( + "AdminMutationResultCellType" + ).addEventListener( + "change", + UpdateAdminMutationResultControlVisibility + ); + + for ( + const ElementId + of [ + "AdminMutationPatternCellType", + "AdminMutationPatternPlant", + "AdminMutationMatcherPlant", + "AdminMutationMatcherTags", + "AdminMutationMatcherTagsAny", + "AdminMutationMatcherTagsNot", + "AdminMutationMatcherCapture" + ] + ) { + document.getElementById( + ElementId + ).addEventListener( + "input", + ApplyAdminMutationSelectedPatternCell + ); + + document.getElementById( + ElementId + ).addEventListener( + "change", + ApplyAdminMutationSelectedPatternCell + ); + } + + for ( + const ElementId + of [ + "AdminMutationResultCellType", + "AdminMutationResultPlant", + "AdminMutationResultCapture" + ] + ) { + document.getElementById( + ElementId + ).addEventListener( + "input", + ApplyAdminMutationSelectedResultCell + ); + + document.getElementById( + ElementId + ).addEventListener( + "change", + ApplyAdminMutationSelectedResultCell + ); + } + + document.getElementById( + "AdminMutationFailureType" + ).addEventListener( + "change", + () => { + UpdateAdminMutationFailureVisibility(); + RenderAdminMutationJsonPreview(); + } + ); + + document.getElementById( + "AdminMutationForm" + ).addEventListener( + "input", + RenderAdminMutationJsonPreview + ); + + document.getElementById( + "AdminMutationForm" + ).addEventListener( + "change", + RenderAdminMutationJsonPreview + ); + + document.getElementById( + "AdminMutationCooldown" + ).addEventListener( + "input", + UpdateAdminMutationCooldownHint + ); +} + + +async function AdminMutationRequest( + 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 + + "/AdminMutations.php", + { + method: "POST", + + headers: { + "Content-Type": + "application/json" + }, + + body: JSON.stringify({ + SaveKey: SaveKey, + Action: Action, + ...Data + }) + } + ); + + let Result; + + try { + Result = await Response.json(); + } catch (Error) { + throw new Error( + "Mutation admin API returned invalid JSON." + ); + } + + if ( + !Response.ok || + Result.Success !== true + ) { + throw new Error( + Result.Error ?? + "Mutation admin request failed." + ); + } + + return Result; +} + + +async function AdminMutationPlantRequest() { + const SaveKey = + localStorage.getItem( + SaveKeyName + ); + + const Response = await fetch( + ApiUrl + + "/AdminPlants.php", + { + method: "POST", + + headers: { + "Content-Type": + "application/json" + }, + + body: JSON.stringify({ + SaveKey: SaveKey, + Action: "List" + }) + } + ); + + const Result = + await Response.json(); + + if ( + !Response.ok || + Result.Success !== true + ) { + throw new Error( + Result.Error ?? + "Couldn't load plants for the Mutation editor." + ); + } + + return Result; +} + + +async function LoadAdminMutationData( + PreferredMutationKey = null +) { + SetAdminMutationMessage( + "Loading mutation data..." + ); + + const [ + PlantResult, + MutationResult + ] = await Promise.all([ + AdminMutationPlantRequest(), + AdminMutationRequest( + "List" + ) + ]); + + AdminMutationPlantCatalogue = + PlantResult.Plants ?? {}; + + AdminMutationCatalogue = + MutationResult.Mutations ?? {}; + + Plants = + AdminMutationPlantCatalogue; + + MutationSets = + AdminMutationCatalogue; + + PopulateAdminMutationPlantControls(); + RenderAdminMutationRelationChecklists(); + RenderAdminMutationSelect(); + + const Keys = + GetSortedAdminMutationKeys(); + + const MutationKey = + PreferredMutationKey !== null && + AdminMutationCatalogue[ + PreferredMutationKey + ] !== undefined + ? PreferredMutationKey + : Keys[0] ?? null; + + if (MutationKey === null) { + StartNewAdminMutation(); + } else { + LoadAdminMutationIntoForm( + MutationKey + ); + } + + SetAdminMutationMessage( + Keys.length === 1 + ? "1 mutation loaded." + : Keys.length + + " mutations loaded." + ); +} + + +function GetSortedAdminMutationKeys() { + return Object.keys( + AdminMutationCatalogue + ).sort( + (A, B) => { + const MutationA = + AdminMutationCatalogue[A]; + + const MutationB = + AdminMutationCatalogue[B]; + + return ( + Number(MutationA.Id) - + Number(MutationB.Id) || + A.localeCompare(B) + ); + } + ); +} + + +function GetSortedAdminPlants() { + return Object.entries( + AdminMutationPlantCatalogue + ).sort( + (A, B) => + Number(A[1].Id) - + Number(B[1].Id) || + A[0].localeCompare(B[0]) + ); +} + + +function RenderAdminMutationSelect() { + const Select = + document.getElementById( + "AdminMutationSelect" + ); + + Select.replaceChildren(); + + for ( + const MutationKey + of GetSortedAdminMutationKeys() + ) { + const Mutation = + AdminMutationCatalogue[ + MutationKey + ]; + + const Option = + document.createElement( + "option" + ); + + Option.value = + MutationKey; + + Option.textContent = + String( + Mutation.Id + ).padStart( + 3, + "0" + ) + + " — " + + Mutation.Name + + ( + Mutation.Archived === true + ? " (archived)" + : "" + ); + + Select.appendChild( + Option + ); + } +} + + +function PopulateAdminMutationPlantControls() { + for ( + const ElementId + of [ + "AdminMutationPatternPlant", + "AdminMutationMatcherPlant", + "AdminMutationResultPlant" + ] + ) { + const Select = + document.getElementById( + ElementId + ); + + Select.replaceChildren(); + + if ( + ElementId === + "AdminMutationMatcherPlant" + ) { + const None = + document.createElement( + "option" + ); + + None.value = ""; + None.textContent = + "Any plant"; + + Select.appendChild( + None + ); + } + + for ( + const [ + PlantKey, + Plant + ] + of GetSortedAdminPlants() + ) { + const Option = + document.createElement( + "option" + ); + + Option.value = + PlantKey; + + Option.textContent = + String( + Plant.Id + ).padStart( + 3, + "0" + ) + + " — " + + Plant.Name; + + Select.appendChild( + Option + ); + } + } +} + + +function RenderAdminMutationRelationChecklists() { + for ( + const [ + ContainerId, + RelationName + ] + of [ + [ + "AdminMutationPlantsUsed", + "PlantsUsed" + ], + [ + "AdminMutationPlantsCreated", + "PlantsCreated" + ] + ] + ) { + const Container = + document.getElementById( + ContainerId + ); + + Container.replaceChildren(); + + for ( + const [ + PlantKey, + Plant + ] + of GetSortedAdminPlants() + ) { + const Label = + document.createElement( + "label" + ); + + Label.className = + "AdminPlantChecklistItem"; + + const Input = + document.createElement( + "input" + ); + + Input.type = + "checkbox"; + + Input.value = + String( + Plant.Id + ); + + Input.dataset.relation = + RelationName; + + Input.dataset.plantKey = + PlantKey; + + Input.addEventListener( + "change", + RenderAdminMutationJsonPreview + ); + + const Text = + document.createElement( + "span" + ); + + Text.textContent = + String( + Plant.Id + ).padStart( + 3, + "0" + ) + + " — " + + Plant.Name; + + Label.append( + Input, + Text + ); + + Container.appendChild( + Label + ); + } + } +} + + +function LoadAdminMutationIntoForm( + MutationKey +) { + const Mutation = + AdminMutationCatalogue[ + MutationKey + ]; + + if (Mutation === undefined) { + return; + } + + AdminMutationEditingKey = + MutationKey; + + document.getElementById( + "AdminMutationSelect" + ).value = MutationKey; + + SetAdminMutationField( + "AdminMutationId", + Mutation.Id + ); + + SetAdminMutationField( + "AdminMutationKey", + MutationKey + ); + + SetAdminMutationField( + "AdminMutationName", + Mutation.Name ?? "" + ); + + SetAdminMutationField( + "AdminMutationDescription", + Mutation.Description ?? "" + ); + + SetAdminMutationField( + "AdminMutationPriority", + Number( + Mutation.Priority ?? 0 + ) + ); + + SetAdminMutationField( + "AdminMutationChance", + Number( + Mutation.Chance ?? 1 + ) * 100 + ); + + SetAdminMutationField( + "AdminMutationCooldown", + Number( + Mutation.Cooldown ?? 0 + ) + ); + + SetAdminMutationField( + "AdminMutationRotation", + Mutation.Rotation ?? + "None" + ); + + document.getElementById( + "AdminMutationAllowImmature" + ).checked = + Mutation.AllowImmature === true; + + AdminMutationPattern = + CloneAdminMutationMatrix( + Mutation.Pattern, + "Any" + ); + + if ( + AdminMutationPattern.length === 0 + ) { + AdminMutationPattern = [ + ["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 = true; + + document.getElementById( + "AdminMutationKey" + ).readOnly = true; + + document.getElementById( + "AdminMutationArchiveNote" + ).hidden = + Mutation.Archived !== true; + + AdminMutationSelectedCell = null; + HideAdminMutationCellEditor(); + RenderAdminMutationGrids(); + UpdateAdminMutationFailureVisibility(); + UpdateAdminMutationCooldownHint(); + RenderAdminMutationJsonPreview(); +} + + +function StartNewAdminMutation() { + AdminMutationEditingKey = null; + + document.getElementById( + "AdminMutationSelect" + ).value = ""; + + SetAdminMutationField( + "AdminMutationId", + GetNextAdminMutationId() + ); + + SetAdminMutationField( + "AdminMutationKey", + "" + ); + + SetAdminMutationField( + "AdminMutationName", + "" + ); + + SetAdminMutationField( + "AdminMutationDescription", + "" + ); + + SetAdminMutationField( + "AdminMutationPriority", + 10 + ); + + SetAdminMutationField( + "AdminMutationChance", + 100 + ); + + SetAdminMutationField( + "AdminMutationCooldown", + 120000 + ); + + SetAdminMutationField( + "AdminMutationRotation", + "Any" + ); + + document.getElementById( + "AdminMutationAllowImmature" + ).checked = false; + + AdminMutationPattern = [ + ["Any", "Empty", "Any"] + ]; + + AdminMutationResult = [ + ["Keep", "Keep", "Keep"] + ]; + + SetAdminMutationField( + "AdminMutationWidth", + 3 + ); + + SetAdminMutationField( + "AdminMutationHeight", + 1 + ); + + SetAdminMutationField( + "AdminMutationFailureType", + "Keep" + ); + + SetAdminMutationField( + "AdminMutationFailureJson", + "[]" + ); + + SetAdminMutationRelations({}); + + document.getElementById( + "AdminMutationId" + ).readOnly = false; + + document.getElementById( + "AdminMutationKey" + ).readOnly = false; + + document.getElementById( + "AdminMutationArchiveNote" + ).hidden = true; + + AdminMutationSelectedCell = null; + HideAdminMutationCellEditor(); + RenderAdminMutationGrids(); + UpdateAdminMutationFailureVisibility(); + UpdateAdminMutationCooldownHint(); + RenderAdminMutationJsonPreview(); + + SetAdminMutationMessage( + "Creating a new mutation." + ); +} + + +function GetNextAdminMutationId() { + return Math.max( + 0, + ...Object.values( + AdminMutationCatalogue + ).map( + Mutation => + Number( + Mutation.Id ?? 0 + ) + ) + ) + 1; +} + + +function SetAdminMutationField( + ElementId, + Value +) { + document.getElementById( + ElementId + ).value = + Value ?? ""; +} + + +function CloneAdminMutationMatrix( + Matrix, + FillValue +) { + if (!Array.isArray(Matrix)) { + return []; + } + + const Width = Math.max( + 0, + ...Matrix.map( + Row => + Array.isArray(Row) + ? Row.length + : 0 + ) + ); + + if (Width === 0) { + return []; + } + + return Matrix.map( + Row => { + const NewRow = []; + + for ( + let X = 0; + X < Width; + X++ + ) { + const Value = + Array.isArray(Row) && + X < Row.length + ? Row[X] + : FillValue; + + NewRow.push( + CloneAdminMutationValue( + Value + ) + ); + } + + return NewRow; + } + ); +} + + +function NormalizeAdminMutationMatrix( + Matrix, + Width, + Height, + FillValue +) { + const Result = []; + + for ( + let Y = 0; + Y < Height; + Y++ + ) { + const Row = []; + + for ( + let X = 0; + X < Width; + X++ + ) { + const Value = + Array.isArray(Matrix?.[Y]) && + X < Matrix[Y].length + ? Matrix[Y][X] + : FillValue; + + Row.push( + CloneAdminMutationValue( + Value + ) + ); + } + + Result.push(Row); + } + + return Result; +} + + +function CloneAdminMutationValue( + Value +) { + if ( + Value !== null && + typeof Value === "object" + ) { + return JSON.parse( + JSON.stringify(Value) + ); + } + + return Value; +} + + +function ResizeAdminMutationRecipe() { + const Width = Math.max( + 1, + Math.min( + 7, + Math.floor( + Number( + document.getElementById( + "AdminMutationWidth" + ).value + ) || 1 + ) + ) + ); + + const Height = Math.max( + 1, + Math.min( + 7, + Math.floor( + Number( + document.getElementById( + "AdminMutationHeight" + ).value + ) || 1 + ) + ) + ); + + SetAdminMutationField( + "AdminMutationWidth", + Width + ); + + SetAdminMutationField( + "AdminMutationHeight", + Height + ); + + AdminMutationPattern = + NormalizeAdminMutationMatrix( + AdminMutationPattern, + Width, + Height, + "Any" + ); + + AdminMutationResult = + NormalizeAdminMutationMatrix( + AdminMutationResult, + Width, + Height, + "Keep" + ); + + AdminMutationSelectedCell = null; + HideAdminMutationCellEditor(); + RenderAdminMutationGrids(); + RenderAdminMutationJsonPreview(); +} + + +function RenderAdminMutationGrids() { + RenderAdminMutationGrid( + "AdminMutationPatternGrid", + "Pattern", + AdminMutationPattern + ); + + RenderAdminMutationGrid( + "AdminMutationResultGrid", + "Result", + AdminMutationResult + ); +} + + +function RenderAdminMutationGrid( + ContainerId, + Mode, + Matrix +) { + const Grid = + document.getElementById( + ContainerId + ); + + Grid.replaceChildren(); + + Grid.style.setProperty( + "--RecipeWidth", + String( + Matrix[0]?.length ?? 1 + ) + ); + + for ( + let Y = 0; + Y < Matrix.length; + Y++ + ) { + for ( + let X = 0; + X < Matrix[Y].length; + X++ + ) { + Grid.appendChild( + CreateAdminMutationGridCell( + Mode, + X, + Y, + Matrix[Y][X] + ) + ); + } + } +} + + +function CreateAdminMutationGridCell( + Mode, + X, + Y, + Value +) { + const Button = + document.createElement( + "button" + ); + + Button.type = "button"; + + Button.className = + "PlantTile GuideRecipeCell AdminMutationGridCell"; + + if ( + AdminMutationSelectedCell !== null && + AdminMutationSelectedCell.Mode === Mode && + AdminMutationSelectedCell.X === X && + AdminMutationSelectedCell.Y === Y + ) { + Button.classList.add( + "AdminMutationGridCellSelected" + ); + } + + const Display = + Mode === "Pattern" + ? GetAdminMutationPatternDisplay( + Value + ) + : GetAdminMutationResultDisplay( + Value + ); + + if (Display.ClassName !== "") { + Button.classList.add( + Display.ClassName + ); + } + + if (Display.Image !== null) { + const Image = + document.createElement( + "img" + ); + + Image.className = + "GuideRecipeImage PlantSprite"; + + Image.src = + Display.Image; + + Image.alt = ""; + + Button.appendChild( + Image + ); + } + + const Label = + document.createElement( + "span" + ); + + Label.className = + "GuideRecipeLabel"; + + Label.textContent = + Display.Label; + + Button.appendChild( + Label + ); + + Button.title = + Display.Label; + + Button.addEventListener( + "click", + () => { + SelectAdminMutationCell( + Mode, + X, + Y + ); + } + ); + + return Button; +} + + +function GetAdminMutationPatternDisplay( + Value +) { + if ( + Value === null || + Value === "Any" + ) { + return { + Label: "Any", + Image: null, + ClassName: + "MutationRecipeAny" + }; + } + + if (Value === "Empty") { + return { + Label: "Empty", + Image: null, + ClassName: + "GuideRecipeEmpty" + }; + } + + if (typeof Value === "string") { + return GetAdminMutationPlantDisplay( + Value, + "GuideRecipePlant" + ); + } + + if ( + Value !== null && + typeof Value === "object" + ) { + const Parts = []; + + if ( + typeof Value.Plant === "string" && + Value.Plant !== "" + ) { + Parts.push( + GetAdminPlantName( + Value.Plant + ) + ); + } + + if ( + Array.isArray(Value.Tags) && + Value.Tags.length > 0 + ) { + Parts.push( + "Tags: " + + Value.Tags.join(", ") + ); + } + + if ( + Array.isArray(Value.TagsAny) && + Value.TagsAny.length > 0 + ) { + Parts.push( + "Any: " + + Value.TagsAny.join(", ") + ); + } + + if ( + Array.isArray(Value.TagsNot) && + Value.TagsNot.length > 0 + ) { + Parts.push( + "Not: " + + Value.TagsNot.join(", ") + ); + } + + if ( + typeof Value.Capture === "string" && + Value.Capture !== "" + ) { + Parts.push( + "$" + + Value.Capture + ); + } + + const Image = + typeof Value.Plant === "string" + ? GetAdminMutationPlantImage( + Value.Plant + ) + : null; + + return { + Label: + Parts.length > 0 + ? Parts.join("\n") + : "Any plant", + Image: Image, + ClassName: + "MutationRecipeMatcher" + }; + } + + return { + Label: "Any", + Image: null, + ClassName: + "MutationRecipeAny" + }; +} + + +function GetAdminMutationResultDisplay( + Value +) { + if ( + Value === null || + Value === "Keep" + ) { + return { + Label: "Keep", + Image: null, + ClassName: + "MutationRecipeKeep" + }; + } + + if (Value === "Empty") { + return { + Label: "Empty", + Image: null, + ClassName: + "GuideRecipeEmpty" + }; + } + + if ( + typeof Value === "string" && + Value.startsWith("$") + ) { + return { + Label: Value, + Image: null, + ClassName: + "MutationRecipeCapture" + }; + } + + if (typeof Value === "string") { + return GetAdminMutationPlantDisplay( + Value, + "GuideRecipePlant" + ); + } + + if ( + Value !== null && + typeof Value === "object" && + typeof Value.Plant === "string" + ) { + if ( + Value.Plant.startsWith("$") + ) { + return { + Label: Value.Plant, + Image: null, + ClassName: + "MutationRecipeCapture" + }; + } + + return GetAdminMutationPlantDisplay( + Value.Plant, + "GuideRecipePlant" + ); + } + + return { + Label: "Keep", + Image: null, + ClassName: + "MutationRecipeKeep" + }; +} + + +function GetAdminMutationPlantDisplay( + PlantKey, + ClassName +) { + return { + Label: + GetAdminPlantName( + PlantKey + ), + Image: + GetAdminMutationPlantImage( + PlantKey + ), + ClassName: + ClassName + }; +} + + +function GetAdminPlantName( + PlantKey +) { + return AdminMutationPlantCatalogue[ + PlantKey + ]?.Name ?? PlantKey; +} + + +function GetAdminMutationPlantImage( + PlantKey +) { + const Images = + PlantImages[ + PlantKey + ] ?? []; + + if ( + !Array.isArray(Images) || + Images.length === 0 + ) { + return null; + } + + return Images[ + Images.length - 1 + ]; +} + + +function SelectAdminMutationCell( + Mode, + X, + Y +) { + AdminMutationSelectedCell = { + Mode: Mode, + X: X, + Y: Y + }; + + const Editor = + document.getElementById( + "AdminMutationCellEditor" + ); + + Editor.hidden = false; + + document.getElementById( + "AdminMutationCellEditorTitle" + ).textContent = + ( + Mode === "Pattern" + ? "Arrange" + : "Result" + ) + + " cell " + + (X + 1) + + ", " + + (Y + 1); + + const PatternControls = + document.getElementById( + "AdminMutationPatternCellControls" + ); + + const ResultControls = + document.getElementById( + "AdminMutationResultCellControls" + ); + + PatternControls.hidden = + Mode !== "Pattern"; + + ResultControls.hidden = + Mode !== "Result"; + + if (Mode === "Pattern") { + LoadAdminMutationPatternCellControls( + AdminMutationPattern[Y][X] + ); + } else { + LoadAdminMutationResultCellControls( + AdminMutationResult[Y][X] + ); + } + + RenderAdminMutationGrids(); +} + + +function HideAdminMutationCellEditor() { + document.getElementById( + "AdminMutationCellEditor" + ).hidden = true; +} + + +function LoadAdminMutationPatternCellControls( + Value +) { + let Type = "Any"; + + if (Value === "Empty") { + Type = "Empty"; + } else if ( + typeof Value === "string" && + Value !== "Any" + ) { + Type = "Plant"; + } else if ( + Value !== null && + typeof Value === "object" + ) { + Type = "Matcher"; + } + + SetAdminMutationField( + "AdminMutationPatternCellType", + Type + ); + + SetAdminMutationField( + "AdminMutationPatternPlant", + Type === "Plant" + ? Value + : GetFirstAdminPlantKey() + ); + + const Matcher = + Type === "Matcher" + ? Value + : {}; + + SetAdminMutationField( + "AdminMutationMatcherPlant", + Matcher.Plant ?? "" + ); + + SetAdminMutationField( + "AdminMutationMatcherTags", + Array.isArray( + Matcher.Tags + ) + ? Matcher.Tags.join(", ") + : "" + ); + + SetAdminMutationField( + "AdminMutationMatcherTagsAny", + Array.isArray( + Matcher.TagsAny + ) + ? Matcher.TagsAny.join(", ") + : "" + ); + + SetAdminMutationField( + "AdminMutationMatcherTagsNot", + Array.isArray( + Matcher.TagsNot + ) + ? Matcher.TagsNot.join(", ") + : "" + ); + + SetAdminMutationField( + "AdminMutationMatcherCapture", + Matcher.Capture ?? "" + ); + + UpdateAdminMutationPatternControlVisibility(); +} + + +function LoadAdminMutationResultCellControls( + Value +) { + let Type = "Keep"; + let PlantKey = + GetFirstAdminPlantKey(); + let Capture = ""; + + if (Value === "Empty") { + Type = "Empty"; + } else if ( + typeof Value === "string" && + Value.startsWith("$") + ) { + Type = "Capture"; + Capture = + Value.slice(1); + } else if ( + typeof Value === "string" && + Value !== "Keep" + ) { + Type = "Plant"; + PlantKey = Value; + } else if ( + Value !== null && + typeof Value === "object" && + typeof Value.Plant === "string" + ) { + if ( + Value.Plant.startsWith("$") + ) { + Type = "Capture"; + Capture = + Value.Plant.slice(1); + } else { + Type = "Plant"; + PlantKey = + Value.Plant; + } + } + + SetAdminMutationField( + "AdminMutationResultCellType", + Type + ); + + SetAdminMutationField( + "AdminMutationResultPlant", + PlantKey + ); + + SetAdminMutationField( + "AdminMutationResultCapture", + Capture + ); + + UpdateAdminMutationResultControlVisibility(); +} + + +function UpdateAdminMutationPatternControlVisibility() { + const Type = + document.getElementById( + "AdminMutationPatternCellType" + ).value; + + document.getElementById( + "AdminMutationPatternPlantRow" + ).hidden = + Type !== "Plant"; + + document.getElementById( + "AdminMutationMatcherRows" + ).hidden = + Type !== "Matcher"; +} + + +function UpdateAdminMutationResultControlVisibility() { + const Type = + document.getElementById( + "AdminMutationResultCellType" + ).value; + + document.getElementById( + "AdminMutationResultPlantRow" + ).hidden = + Type !== "Plant"; + + document.getElementById( + "AdminMutationResultCaptureRow" + ).hidden = + Type !== "Capture"; +} + + +function ApplyAdminMutationSelectedPatternCell() { + if ( + AdminMutationSelectedCell === null || + AdminMutationSelectedCell.Mode !== + "Pattern" + ) { + return; + } + + const Type = + document.getElementById( + "AdminMutationPatternCellType" + ).value; + + let Value = "Any"; + + if (Type === "Empty") { + Value = "Empty"; + } else if (Type === "Plant") { + Value = + document.getElementById( + "AdminMutationPatternPlant" + ).value; + } else if (Type === "Matcher") { + const Matcher = {}; + + const Plant = + document.getElementById( + "AdminMutationMatcherPlant" + ).value; + + if (Plant !== "") { + Matcher.Plant = Plant; + } + + AddAdminMutationTagField( + Matcher, + "Tags", + "AdminMutationMatcherTags" + ); + + AddAdminMutationTagField( + Matcher, + "TagsAny", + "AdminMutationMatcherTagsAny" + ); + + AddAdminMutationTagField( + Matcher, + "TagsNot", + "AdminMutationMatcherTagsNot" + ); + + const Capture = + document.getElementById( + "AdminMutationMatcherCapture" + ).value.trim(); + + if (Capture !== "") { + Matcher.Capture = + Capture; + } + + Value = Matcher; + } + + AdminMutationPattern[ + AdminMutationSelectedCell.Y + ][ + AdminMutationSelectedCell.X + ] = Value; + + UpdateAdminMutationPatternControlVisibility(); + RenderAdminMutationGrids(); + RenderAdminMutationJsonPreview(); +} + + +function ApplyAdminMutationSelectedResultCell() { + if ( + AdminMutationSelectedCell === null || + AdminMutationSelectedCell.Mode !== + "Result" + ) { + return; + } + + const Type = + document.getElementById( + "AdminMutationResultCellType" + ).value; + + let Value = "Keep"; + + if (Type === "Empty") { + Value = "Empty"; + } else if (Type === "Plant") { + Value = + document.getElementById( + "AdminMutationResultPlant" + ).value; + } else if (Type === "Capture") { + const Capture = + document.getElementById( + "AdminMutationResultCapture" + ).value.trim(); + + Value = + Capture === "" + ? "Keep" + : "$" + Capture; + } + + AdminMutationResult[ + AdminMutationSelectedCell.Y + ][ + AdminMutationSelectedCell.X + ] = Value; + + UpdateAdminMutationResultControlVisibility(); + RenderAdminMutationGrids(); + RenderAdminMutationJsonPreview(); +} + + +function AddAdminMutationTagField( + Matcher, + PropertyName, + ElementId +) { + const Tags = + ParseAdminMutationList( + document.getElementById( + ElementId + ).value + ); + + if (Tags.length > 0) { + Matcher[ + PropertyName + ] = Tags; + } +} + + +function ParseAdminMutationList( + Value +) { + return [ + ...new Set( + String(Value) + .split(",") + .map( + Item => + Item.trim() + ) + .filter( + Item => + Item.length > 0 + ) + ) + ]; +} + + +function GetFirstAdminPlantKey() { + return GetSortedAdminPlants()[0] + ?.[0] ?? ""; +} + + +function LoadAdminMutationFailure( + Failure +) { + if (Failure === "Clear") { + SetAdminMutationField( + "AdminMutationFailureType", + "Clear" + ); + + SetAdminMutationField( + "AdminMutationFailureJson", + "[]" + ); + + return; + } + + if (Array.isArray(Failure)) { + SetAdminMutationField( + "AdminMutationFailureType", + "Custom" + ); + + SetAdminMutationField( + "AdminMutationFailureJson", + JSON.stringify( + Failure, + null, + 4 + ) + ); + + return; + } + + SetAdminMutationField( + "AdminMutationFailureType", + "Keep" + ); + + SetAdminMutationField( + "AdminMutationFailureJson", + "[]" + ); +} + + +function UpdateAdminMutationFailureVisibility() { + const Type = + document.getElementById( + "AdminMutationFailureType" + ).value; + + document.getElementById( + "AdminMutationFailureJsonRow" + ).hidden = + Type !== "Custom"; +} + + +function GetAdminMutationFailure() { + const Type = + document.getElementById( + "AdminMutationFailureType" + ).value; + + if (Type === "Keep") { + return "Keep"; + } + + if (Type === "Clear") { + return "Clear"; + } + + const Raw = + document.getElementById( + "AdminMutationFailureJson" + ).value.trim(); + + const Failure = + JSON.parse( + Raw === "" + ? "[]" + : Raw + ); + + if (!Array.isArray(Failure)) { + throw new Error( + "Custom failure result must be a JSON matrix." + ); + } + + return Failure; +} + + +function SetAdminMutationRelations( + Relations +) { + const Used = new Set( + Relations.PlantsUsed ?? [] + ); + + const Created = new Set( + Relations.PlantsCreated ?? [] + ); + + for ( + const Input + of document.querySelectorAll( + ".AdminPlantChecklist input" + ) + ) { + const PlantId = + Number(Input.value); + + Input.checked = + Input.dataset.relation === + "PlantsUsed" + ? Used.has(PlantId) + : Created.has(PlantId); + } +} + + +function GetAdminMutationRelation( + RelationName +) { + return [ + ...document.querySelectorAll( + '.AdminPlantChecklist input[data-relation="' + + RelationName + + '"]:checked' + ) + ] + .map( + Input => + Number(Input.value) + ) + .sort( + (A, B) => A - B + ); +} + + +function GetAdminMutationFormData() { + return { + Id: Number( + document.getElementById( + "AdminMutationId" + ).value + ), + + MutationKey: + document.getElementById( + "AdminMutationKey" + ).value.trim(), + + Name: + document.getElementById( + "AdminMutationName" + ).value.trim(), + + Description: + document.getElementById( + "AdminMutationDescription" + ).value.trim(), + + Priority: Number( + document.getElementById( + "AdminMutationPriority" + ).value + ), + + Chance: + Number( + document.getElementById( + "AdminMutationChance" + ).value + ) / + 100, + + Cooldown: Number( + document.getElementById( + "AdminMutationCooldown" + ).value + ), + + Rotation: + document.getElementById( + "AdminMutationRotation" + ).value, + + AllowImmature: + document.getElementById( + "AdminMutationAllowImmature" + ).checked, + + Pattern: + CloneAdminMutationValue( + AdminMutationPattern + ), + + Success: + CloneAdminMutationValue( + AdminMutationResult + ), + + Failure: + GetAdminMutationFailure(), + + Relations: { + PlantsUsed: + GetAdminMutationRelation( + "PlantsUsed" + ), + + PlantsCreated: + GetAdminMutationRelation( + "PlantsCreated" + ) + }, + + IsNew: + AdminMutationEditingKey === + null + }; +} + + +async function SaveAdminMutation( + Event +) { + Event.preventDefault(); + + if (AdminMutationSavePending) { + return; + } + + const Form = + document.getElementById( + "AdminMutationForm" + ); + + if (!Form.reportValidity()) { + return; + } + + let Mutation; + + try { + Mutation = + GetAdminMutationFormData(); + } catch (Error) { + SetAdminMutationMessage( + Error.message + ); + + return; + } + + AdminMutationSavePending = true; + + const SaveButton = + document.getElementById( + "AdminMutationSaveButton" + ); + + SaveButton.disabled = true; + SaveButton.textContent = + "Saving..."; + + SetAdminMutationMessage( + "Saving mutation..." + ); + + try { + const Result = + await AdminMutationRequest( + "Save", + { + Mutation: Mutation + } + ); + + localStorage.removeItem( + "SarahtoninGardenContent" + ); + + await LoadAdminMutationData( + Result.MutationKey ?? + Mutation.MutationKey + ); + + SetAdminMutationMessage( + "Mutation saved." + ); + } catch (Error) { + console.error( + "Couldn't save mutation:", + Error + ); + + SetAdminMutationMessage( + Error.message ?? + "Couldn't save mutation." + ); + } finally { + AdminMutationSavePending = false; + SaveButton.disabled = false; + SaveButton.textContent = + "Save mutation"; + } +} + + +function RenderAdminMutationJsonPreview() { + const Preview = + document.getElementById( + "AdminMutationJsonPreview" + ); + + if (Preview === null) { + return; + } + + try { + Preview.textContent = + JSON.stringify( + GetAdminMutationFormData(), + null, + 4 + ); + } catch (Error) { + Preview.textContent = + "Invalid editor data: " + + Error.message; + } +} + + +function UpdateAdminMutationCooldownHint() { + const Cooldown = Number( + document.getElementById( + "AdminMutationCooldown" + ).value + ); + + document.getElementById( + "AdminMutationCooldownHint" + ).textContent = + Number.isFinite(Cooldown) + ? FormatAdminMutationDuration( + Cooldown + ) + : ""; +} + + +function FormatAdminMutationDuration( + Milliseconds +) { + Milliseconds = Math.max( + 0, + Number(Milliseconds) + ); + + if (Milliseconds < 1000) { + return Milliseconds + "ms"; + } + + const TotalSeconds = + Math.round( + Milliseconds / + 1000 + ); + + const Hours = + Math.floor( + TotalSeconds / + 3600 + ); + + const Minutes = + Math.floor( + ( + TotalSeconds % + 3600 + ) / + 60 + ); + + const Seconds = + TotalSeconds % 60; + + const Parts = []; + + if (Hours > 0) { + Parts.push( + Hours + "h" + ); + } + + if (Minutes > 0) { + Parts.push( + Minutes + "m" + ); + } + + if ( + Seconds > 0 || + Parts.length === 0 + ) { + Parts.push( + Seconds + "s" + ); + } + + return Parts.join(" "); +} + + +function SetAdminMutationMessage( + Message +) { + const Element = + document.getElementById( + "AdminMutationMessage" + ); + + if (Element !== null) { + Element.textContent = + Message; + } +} + + +document.addEventListener( + "DOMContentLoaded", + StartAdminMutationEditor +); diff --git a/Scripts/Garden/AdminPlantEditor.js b/Scripts/Garden/AdminPlantEditor.js new file mode 100644 index 0000000..53cfc36 --- /dev/null +++ b/Scripts/Garden/AdminPlantEditor.js @@ -0,0 +1,863 @@ +let AdminPlantCatalogue = {}; +let AdminPlantEditingKey = null; +let AdminPlantSavePending = false; + + +async function StartAdminPlantEditor() { + try { + const Status = + await GetAdminStatus(); + + if ( + Status.Success !== true || + Status.IsAdmin !== true + ) { + SetAdminPlantMessage( + "Admin access required." + ); + + return; + } + + BindAdminPlantEditor(); + await LoadAdminPlants(); + } catch (Error) { + console.error( + "Couldn't start Plant editor:", + Error + ); + + SetAdminPlantMessage( + Error.message ?? + "Couldn't load the Plant editor." + ); + } +} + + +function BindAdminPlantEditor() { + document.getElementById( + "AdminPlantSelect" + ).addEventListener( + "change", + Event => { + const PlantKey = + Event.target.value; + + if (PlantKey === "") { + StartNewAdminPlant(); + return; + } + + LoadAdminPlantIntoForm( + PlantKey + ); + } + ); + + document.getElementById( + "AdminPlantNewButton" + ).addEventListener( + "click", + StartNewAdminPlant + ); + + document.getElementById( + "AdminPlantForm" + ).addEventListener( + "submit", + SaveAdminPlant + ); + + for ( + const ElementId + of [ + "AdminPlantId", + "AdminPlantKey", + "AdminPlantName", + "AdminPlantGrowthTime", + "AdminPlantShopPlant", + "AdminPlantBaseCost" + ] + ) { + document.getElementById( + ElementId + ).addEventListener( + "input", + RenderAdminPlantPreview + ); + + document.getElementById( + ElementId + ).addEventListener( + "change", + RenderAdminPlantPreview + ); + } + + document.getElementById( + "AdminPlantShopPlant" + ).addEventListener( + "change", + UpdateAdminPlantShopFields + ); +} + + +async function AdminPlantRequest( + Action, + Data = {} +) { + const SaveKey = + localStorage.getItem( + SaveKeyName + ); + + if ( + typeof SaveKey !== "string" || + !/^[0-9A-Fa-f]{64}$/.test( + SaveKey + ) + ) { + throw new Error( + "A valid Account Key is required." + ); + } + + const Response = await fetch( + ApiUrl + + "/AdminPlants.php", + { + method: "POST", + + headers: { + "Content-Type": + "application/json" + }, + + body: JSON.stringify({ + SaveKey: SaveKey, + Action: Action, + ...Data + }) + } + ); + + let Result; + + try { + Result = await Response.json(); + } catch (Error) { + throw new Error( + "Plant admin API returned invalid JSON." + ); + } + + if ( + !Response.ok || + Result.Success !== true + ) { + throw new Error( + Result.Error ?? + "Plant admin request failed." + ); + } + + return Result; +} + + +async function LoadAdminPlants( + PreferredPlantKey = null +) { + SetAdminPlantMessage( + "Loading plants..." + ); + + const Result = + await AdminPlantRequest( + "List" + ); + + AdminPlantCatalogue = + Result.Plants ?? {}; + + Plants = + AdminPlantCatalogue; + + RenderAdminPlantSelect(); + + const Keys = + GetSortedAdminPlantKeys(); + + const PlantKey = + PreferredPlantKey !== null && + AdminPlantCatalogue[ + PreferredPlantKey + ] !== undefined + ? PreferredPlantKey + : Keys[0] ?? null; + + if (PlantKey === null) { + StartNewAdminPlant(); + } else { + LoadAdminPlantIntoForm( + PlantKey + ); + } + + SetAdminPlantMessage( + Keys.length === 1 + ? "1 plant loaded." + : Keys.length + + " plants loaded." + ); +} + + +function GetSortedAdminPlantKeys() { + return Object.keys( + AdminPlantCatalogue + ).sort( + (A, B) => { + const PlantA = + AdminPlantCatalogue[A]; + + const PlantB = + AdminPlantCatalogue[B]; + + return ( + Number(PlantA.Id) - + Number(PlantB.Id) || + A.localeCompare(B) + ); + } + ); +} + + +function RenderAdminPlantSelect() { + const Select = + document.getElementById( + "AdminPlantSelect" + ); + + Select.replaceChildren(); + + for ( + const PlantKey + of GetSortedAdminPlantKeys() + ) { + const Plant = + AdminPlantCatalogue[ + PlantKey + ]; + + const Option = + document.createElement( + "option" + ); + + Option.value = + PlantKey; + + Option.textContent = + String( + Plant.Id + ).padStart( + 3, + "0" + ) + + " — " + + Plant.Name + + ( + Plant.Archived === true + ? " (archived)" + : "" + ); + + Select.appendChild( + Option + ); + } +} + + +function LoadAdminPlantIntoForm( + PlantKey +) { + const Plant = + AdminPlantCatalogue[ + PlantKey + ]; + + if (Plant === undefined) { + return; + } + + AdminPlantEditingKey = + PlantKey; + + document.getElementById( + "AdminPlantSelect" + ).value = PlantKey; + + SetAdminPlantField( + "AdminPlantId", + Plant.Id + ); + + SetAdminPlantField( + "AdminPlantKey", + PlantKey + ); + + SetAdminPlantField( + "AdminPlantName", + Plant.Name ?? "" + ); + + SetAdminPlantField( + "AdminPlantDescription", + Plant.Description ?? "" + ); + + SetAdminPlantField( + "AdminPlantTags", + Array.isArray( + Plant.Tags + ) + ? Plant.Tags.join(", ") + : "" + ); + + SetAdminPlantField( + "AdminPlantGrowthTime", + Number( + Plant.GrowthTime ?? 0 + ) + ); + + document.getElementById( + "AdminPlantShopPlant" + ).checked = + Plant.Shop?.ShopPlant === true; + + SetAdminPlantField( + "AdminPlantBaseCost", + Plant.Shop?.BaseCost ?? "" + ); + + SetAdminPlantField( + "AdminPlantEffects", + JSON.stringify( + Plant.Effects ?? {}, + null, + 4 + ) + ); + + document.getElementById( + "AdminPlantId" + ).readOnly = true; + + document.getElementById( + "AdminPlantKey" + ).readOnly = true; + + document.getElementById( + "AdminPlantArchiveNote" + ).hidden = + Plant.Archived !== true; + + UpdateAdminPlantShopFields(); + RenderAdminPlantPreview(); +} + + +function StartNewAdminPlant() { + AdminPlantEditingKey = null; + + document.getElementById( + "AdminPlantSelect" + ).value = ""; + + SetAdminPlantField( + "AdminPlantId", + GetNextAdminPlantId() + ); + + SetAdminPlantField( + "AdminPlantKey", + "" + ); + + SetAdminPlantField( + "AdminPlantName", + "" + ); + + SetAdminPlantField( + "AdminPlantDescription", + "" + ); + + SetAdminPlantField( + "AdminPlantTags", + "" + ); + + SetAdminPlantField( + "AdminPlantGrowthTime", + 3600000 + ); + + document.getElementById( + "AdminPlantShopPlant" + ).checked = false; + + SetAdminPlantField( + "AdminPlantBaseCost", + "" + ); + + SetAdminPlantField( + "AdminPlantEffects", + "{}" + ); + + document.getElementById( + "AdminPlantId" + ).readOnly = false; + + document.getElementById( + "AdminPlantKey" + ).readOnly = false; + + document.getElementById( + "AdminPlantArchiveNote" + ).hidden = true; + + UpdateAdminPlantShopFields(); + RenderAdminPlantPreview(); + SetAdminPlantMessage( + "Creating a new plant." + ); +} + + +function GetNextAdminPlantId() { + return Math.max( + 0, + ...Object.values( + AdminPlantCatalogue + ).map( + Plant => + Number( + Plant.Id ?? 0 + ) + ) + ) + 1; +} + + +function SetAdminPlantField( + ElementId, + Value +) { + document.getElementById( + ElementId + ).value = + Value ?? ""; +} + + +function UpdateAdminPlantShopFields() { + const ShopPlant = + document.getElementById( + "AdminPlantShopPlant" + ).checked; + + const BaseCost = + document.getElementById( + "AdminPlantBaseCost" + ); + + BaseCost.disabled = + !ShopPlant; + + BaseCost.required = + ShopPlant; +} + + +function ParseAdminPlantTags() { + const Value = + document.getElementById( + "AdminPlantTags" + ).value; + + return [ + ...new Set( + Value + .split(",") + .map( + Tag => + Tag.trim() + ) + .filter( + Tag => + Tag.length > 0 + ) + ) + ]; +} + + +function ParseAdminPlantEffects() { + const Value = + document.getElementById( + "AdminPlantEffects" + ).value.trim(); + + if (Value === "") { + return {}; + } + + const Effects = + JSON.parse(Value); + + if ( + Effects === null || + typeof Effects !== "object" || + Array.isArray(Effects) + ) { + throw new Error( + "Effects JSON must be an object." + ); + } + + return Effects; +} + + +function GetAdminPlantFormData() { + const ShopPlant = + document.getElementById( + "AdminPlantShopPlant" + ).checked; + + const BaseCostValue = + document.getElementById( + "AdminPlantBaseCost" + ).value; + + return { + Id: Number( + document.getElementById( + "AdminPlantId" + ).value + ), + + PlantKey: + document.getElementById( + "AdminPlantKey" + ).value.trim(), + + Name: + document.getElementById( + "AdminPlantName" + ).value.trim(), + + Description: + document.getElementById( + "AdminPlantDescription" + ).value.trim(), + + Tags: + ParseAdminPlantTags(), + + GrowthTime: Number( + document.getElementById( + "AdminPlantGrowthTime" + ).value + ), + + Effects: + ParseAdminPlantEffects(), + + ShopPlant: ShopPlant, + + BaseCost: + ShopPlant + ? Number(BaseCostValue) + : null, + + IsNew: + AdminPlantEditingKey === + null + }; +} + + +async function SaveAdminPlant( + Event +) { + Event.preventDefault(); + + if (AdminPlantSavePending) { + return; + } + + const Form = + document.getElementById( + "AdminPlantForm" + ); + + if (!Form.reportValidity()) { + return; + } + + let Plant; + + try { + Plant = + GetAdminPlantFormData(); + } catch (Error) { + SetAdminPlantMessage( + Error.message + ); + + return; + } + + AdminPlantSavePending = true; + + const SaveButton = + document.getElementById( + "AdminPlantSaveButton" + ); + + SaveButton.disabled = true; + SaveButton.textContent = + "Saving..."; + + SetAdminPlantMessage( + "Saving plant..." + ); + + try { + const Result = + await AdminPlantRequest( + "Save", + { + Plant: Plant + } + ); + + localStorage.removeItem( + "SarahtoninGardenContent" + ); + + await LoadAdminPlants( + Result.PlantKey ?? + Plant.PlantKey + ); + + SetAdminPlantMessage( + "Plant saved." + ); + } catch (Error) { + console.error( + "Couldn't save plant:", + Error + ); + + SetAdminPlantMessage( + Error.message ?? + "Couldn't save plant." + ); + } finally { + AdminPlantSavePending = false; + SaveButton.disabled = false; + SaveButton.textContent = + "Save plant"; + } +} + + +function RenderAdminPlantPreview() { + const PlantKey = + document.getElementById( + "AdminPlantKey" + ).value.trim(); + + const Name = + document.getElementById( + "AdminPlantName" + ).value.trim() || + "New plant"; + + const Tile = + document.getElementById( + "AdminPlantPreviewTile" + ); + + Tile.replaceChildren(); + + const Images = + PlantImages[ + PlantKey + ] ?? []; + + if ( + Array.isArray(Images) && + Images.length > 0 + ) { + const Image = + document.createElement( + "img" + ); + + Image.className = + "PlantSprite"; + + Image.src = + Images[ + Images.length - 1 + ]; + + Image.alt = Name; + + Tile.appendChild( + Image + ); + } else { + const Missing = + document.createElement( + "span" + ); + + Missing.className = + "AdminPlantPreviewMissing"; + + Missing.textContent = + PlantKey === "" + ? "No key" + : "No sprite"; + + Tile.appendChild( + Missing + ); + } + + document.getElementById( + "AdminPlantPreviewName" + ).textContent = Name; + + document.getElementById( + "AdminPlantPreviewKey" + ).textContent = + PlantKey === "" + ? "Plant Key not set" + : PlantKey; + + const GrowthTime = Number( + document.getElementById( + "AdminPlantGrowthTime" + ).value + ); + + document.getElementById( + "AdminPlantGrowthTimeHint" + ).textContent = + Number.isFinite(GrowthTime) + ? FormatAdminDuration( + GrowthTime + ) + : ""; +} + + +function FormatAdminDuration( + Milliseconds +) { + Milliseconds = Math.max( + 0, + Number(Milliseconds) + ); + + if (Milliseconds < 1000) { + return Milliseconds + "ms"; + } + + const TotalSeconds = + Math.round( + Milliseconds / + 1000 + ); + + const Hours = + Math.floor( + TotalSeconds / + 3600 + ); + + const Minutes = + Math.floor( + ( + TotalSeconds % + 3600 + ) / + 60 + ); + + const Seconds = + TotalSeconds % 60; + + const Parts = []; + + if (Hours > 0) { + Parts.push( + Hours + "h" + ); + } + + if (Minutes > 0) { + Parts.push( + Minutes + "m" + ); + } + + if ( + Seconds > 0 || + Parts.length === 0 + ) { + Parts.push( + Seconds + "s" + ); + } + + return Parts.join(" "); +} + + +function SetAdminPlantMessage( + Message +) { + const Element = + document.getElementById( + "AdminPlantMessage" + ); + + if (Element !== null) { + Element.textContent = + Message; + } +} + + +document.addEventListener( + "DOMContentLoaded", + StartAdminPlantEditor +); diff --git a/Styles/Style.css b/Styles/Style.css index 2535607..4ac7417 100644 --- a/Styles/Style.css +++ b/Styles/Style.css @@ -1722,3 +1722,481 @@ select { text-align: left; } } + + +/* + * Admin editors + */ + +.AdminEditorNavigation { + display: grid; + grid-template-columns: + repeat( + 2, + minmax(0, 1fr) + ); + gap: 1rem; + + margin-top: 1.5rem; +} + +.AdminEditorLink { + display: flex; + flex-direction: column; + gap: 0.35rem; + + min-height: 7rem; + + align-items: flex-start; + justify-content: center; + + padding: 1rem; + + text-decoration: none; +} + +.AdminEditorLink strong { + color: var(--Green); + font-size: 1.2rem; +} + +.AdminEditorLink span { + color: var(--Subtext1); + line-height: 1.35; +} + +.AdminEditorToolbar { + display: grid; + grid-template-columns: + auto + minmax(0, 1fr) + auto; + gap: 0.75rem; + + align-items: center; + + margin: 1.5rem 0 1rem; +} + +.AdminEditorToolbar label { + color: var(--Blue); +} + +.AdminEditorForm { + padding: 1rem; +} + +.AdminEditorGrid { + display: grid; + grid-template-columns: + repeat( + 2, + minmax(0, 1fr) + ); + gap: 1rem; +} + +.AdminEditorField { + display: flex; + flex-direction: column; + gap: 0.4rem; + + min-width: 0; +} + +.AdminEditorField > span, +.AdminMutationDimensions label { + color: var(--Blue); +} + +.AdminEditorField small { + color: var(--Overlay2); + line-height: 1.25; +} + +.AdminEditorWideField { + grid-column: 1 / -1; +} + +.AdminEditorForm input, +.AdminEditorForm textarea, +.AdminEditorForm select, +.AdminEditorToolbar select, +.AdminMutationDimensions input { + width: 100%; + min-width: 0; + padding: 0.55rem; + + color: var(--Text); + background-color: var(--Surface0); + + border: 3px solid var(--Surface2); + border-radius: 0; + + outline: none; +} + +.AdminEditorForm textarea { + resize: vertical; +} + +.AdminEditorForm input:focus, +.AdminEditorForm textarea:focus, +.AdminEditorForm select:focus, +.AdminEditorToolbar select:focus, +.AdminMutationDimensions input:focus { + border-color: var(--Sky); +} + +.AdminEditorForm input:read-only { + color: var(--Subtext0); + background-color: var(--Base); +} + +.AdminEditorForm input:disabled { + opacity: 0.55; +} + +.AdminEditorCheckField { + display: flex; + gap: 0.65rem; + + align-items: center; + align-self: end; + + min-height: 2.8rem; +} + +.AdminEditorCheckField input { + width: 1.2rem; + height: 1.2rem; + margin: 0; + + accent-color: var(--Green); +} + +.AdminInlineButton { + width: auto; + min-width: max-content; +} + +.AdminSaveButton { + --ActionAccent: var(--Green); + + width: auto; +} + +.AdminEditorBackLink { + display: inline-flex; + + align-items: center; + justify-content: center; + + text-decoration: none; +} + +.AdminEditorActions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + + margin-top: 1.25rem; +} + +.AdminEditorPreview, +.AdminMutationRecipeSection, +.AdminMutationRelationsSection, +.AdminMutationFailureSection { + margin-top: 1.5rem; + padding-top: 1rem; + + border-top: 3px solid var(--Surface2); +} + +.AdminEditorPreview h2, +.AdminMutationRecipeSection h2, +.AdminMutationRelationsSection h2, +.AdminMutationFailureSection h2 { + margin-bottom: 0.5rem; + + font-size: 1.4rem; +} + +.AdminPlantPreviewRow { + display: flex; + gap: 1rem; + + align-items: center; +} + +.AdminPlantPreviewTile { + display: flex; + + width: 7rem; + height: 7rem; + + flex: 0 0 auto; + align-items: center; + justify-content: center; +} + +.AdminPlantPreviewMissing { + padding: 0.5rem; + + color: var(--Overlay1); + text-align: center; +} + +#AdminPlantPreviewKey, +#AdminPlantArchiveNote, +#AdminMutationArchiveNote { + margin: 0.35rem 0 0; + + color: var(--Overlay2); +} + +#AdminPlantArchiveNote, +#AdminMutationArchiveNote { + color: var(--Yellow); +} + +.AdminMutationRecipeHeader { + display: flex; + justify-content: space-between; + gap: 1rem; + + align-items: end; +} + +.AdminMutationRecipeHeader p, +.AdminMutationRelationsSection > p { + margin-bottom: 0; + + color: var(--Subtext1); +} + +.AdminMutationDimensions { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + + align-items: end; +} + +.AdminMutationDimensions label { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.AdminMutationDimensions input { + width: 5rem; +} + +.AdminMutationRecipeFlow { + display: grid; + grid-template-columns: + minmax(0, 1fr) + auto + minmax(0, 1fr); + gap: 0.75rem; + + align-items: center; + + margin-top: 1rem; +} + +.AdminMutationGridPanel { + min-width: 0; +} + +.AdminMutationGridPanel h3 { + margin-bottom: 0.25rem; + + color: var(--Subtext1); + + font-size: 1rem; + text-align: center; +} + +.AdminMutationGrid { + width: 100%; + margin: 0.5rem auto; +} + +.AdminMutationGridCell { + width: 100%; + + cursor: pointer; +} + +.AdminMutationGridCell:hover { + border-color: var(--Sky); +} + +.AdminMutationGridCellSelected { + border-color: var(--Yellow) !important; + box-shadow: + 0 0 0 2px + var(--Crust); +} + +.AdminMutationCellEditor { + margin-top: 1rem; + padding: 1rem; + + background-color: var(--Base); + border: 3px solid var(--Yellow); +} + +.AdminMutationCellEditor h3 { + margin-bottom: 0.75rem; + + color: var(--Yellow); +} + +#AdminMutationPatternCellControls, +#AdminMutationResultCellControls, +.AdminMatcherFields { + display: grid; + grid-template-columns: + repeat( + 2, + minmax(0, 1fr) + ); + gap: 0.75rem; +} + +.AdminMatcherFields { + grid-column: 1 / -1; +} + +.AdminMutationRelationColumns { + display: grid; + grid-template-columns: + repeat( + 2, + minmax(0, 1fr) + ); + gap: 1rem; + + margin-top: 1rem; +} + +.AdminMutationRelationColumns fieldset { + min-width: 0; + margin: 0; + padding: 0.75rem; + + background-color: var(--Base); + border: 3px solid var(--Surface2); +} + +.AdminMutationRelationColumns legend { + padding: 0 0.35rem; + + color: var(--Peach); +} + +.AdminPlantChecklist { + display: flex; + flex-direction: column; + gap: 0.45rem; + + max-height: 14rem; + overflow-y: auto; +} + +.AdminPlantChecklistItem { + display: flex; + gap: 0.55rem; + + align-items: center; +} + +.AdminPlantChecklistItem input { + width: 1rem; + height: 1rem; + margin: 0; + + accent-color: var(--Green); +} + +.AdminGeneratedJson { + margin-top: 1.5rem; + padding: 0.75rem; + + background-color: var(--Base); + border: 3px solid var(--Surface2); +} + +.AdminGeneratedJson summary { + cursor: pointer; + color: var(--Sky); +} + +.AdminGeneratedJson pre { + max-height: 24rem; + margin: 0.75rem 0 0; + padding: 0.75rem; + + overflow: auto; + + color: var(--Subtext1); + background-color: var(--Crust); + + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +@media (max-width: 700px) { + .AdminEditorNavigation, + .AdminEditorGrid, + #AdminMutationPatternCellControls, + #AdminMutationResultCellControls, + .AdminMatcherFields, + .AdminMutationRelationColumns { + grid-template-columns: 1fr; + } + + .AdminEditorToolbar { + grid-template-columns: 1fr; + } + + .AdminEditorToolbar label { + margin-bottom: -0.35rem; + } + + .AdminInlineButton, + .AdminSaveButton { + width: 100%; + } + + .AdminMutationRecipeHeader { + flex-direction: column; + align-items: stretch; + } + + .AdminMutationRecipeFlow { + grid-template-columns: 1fr; + } + + .AdminMutationRecipeFlow > + .MutationEncyclopediaRecipeArrow { + justify-self: center; + + transform: rotate(90deg); + } + + .AdminMutationDimensions { + align-items: stretch; + } + + .AdminMutationDimensions label { + flex: 1 1 5rem; + } + + .AdminMutationDimensions input { + width: 100%; + } +}