diff --git a/Assets/Img/Mutations.png b/Assets/Img/Mutations.png new file mode 100644 index 0000000..c43d52f Binary files /dev/null and b/Assets/Img/Mutations.png differ diff --git a/Pages/Bits/Navbar.php b/Pages/Bits/Navbar.php index 2ff4219..91051b0 100644 --- a/Pages/Bits/Navbar.php +++ b/Pages/Bits/Navbar.php @@ -6,6 +6,7 @@ $NavbarPages = [ "Garden" => "/Pages/Garden.html", "Shop" => "/Pages/Shop.html", "Plants" => "/Pages/Plants.html", + "Mutations" => "/Pages/Mutations.html", "Guide" => "/Pages/Guide.html", "Profile" => "/Pages/Profile.html" ]; diff --git a/Pages/Mutations.php b/Pages/Mutations.php new file mode 100644 index 0000000..ee3bc8a --- /dev/null +++ b/Pages/Mutations.php @@ -0,0 +1,48 @@ + + + + + + + + + + +
+

Mutation encyclopedia

+ +

+ Every mutation you discover is recorded here. + Once found, its recipe and result stay visible so + you can recreate it whenever you want. +

+ +
+ +

+ Loading discovered mutations... +

+
+ + + + + + + + + + diff --git a/Scripts/Garden/MutationEncyclopedia.js b/Scripts/Garden/MutationEncyclopedia.js new file mode 100644 index 0000000..d46b882 --- /dev/null +++ b/Scripts/Garden/MutationEncyclopedia.js @@ -0,0 +1,1161 @@ +let MutationEncyclopediaSave; + + +async function StartMutationEncyclopedia() { + try { + await LoadGameContent(); + + MutationEncyclopediaSave = + await LoadGame(); + + RenderMutationEncyclopedia(); + } catch (Error) { + console.error( + "Couldn't load Mutation encyclopedia:", + Error + ); + + SetMutationEncyclopediaMessage( + "Couldn't load your discovered mutations." + ); + } +} + + +function RenderMutationEncyclopedia() { + const List = + document.getElementById( + "MutationEncyclopediaList" + ); + + List.replaceChildren(); + + + const DiscoveredMutations = + Object.values( + MutationSets + ) + .filter( + Mutation => + HasDiscoveredMutation( + MutationEncyclopediaSave, + Mutation.Id + ) + ) + .sort( + (A, B) => + A.Id - B.Id + ); + + + if ( + DiscoveredMutations.length === 0 + ) { + SetMutationEncyclopediaMessage( + "No mutations discovered yet." + ); + + return; + } + + + for ( + const Mutation + of DiscoveredMutations + ) { + List.appendChild( + CreateMutationEncyclopediaCard( + Mutation + ) + ); + } + + + SetMutationEncyclopediaMessage( + DiscoveredMutations.length === 1 + ? "1 mutation discovered." + : DiscoveredMutations.length + .toLocaleString() + + " mutations discovered." + ); +} + + +function CreateMutationEncyclopediaCard( + Mutation +) { + const Card = + document.createElement( + "article" + ); + + Card.className = + "Panel MutationEncyclopediaCard"; + + Card.id = + "Mutation-" + + Mutation.Id; + + + const Header = + document.createElement( + "header" + ); + + Header.className = + "MutationEncyclopediaHeader"; + + + const Number = + document.createElement( + "span" + ); + + Number.className = + "MutationEncyclopediaNumber"; + + Number.textContent = + String( + Mutation.Id + ).padStart( + 3, + "0" + ); + + + const Name = + document.createElement( + "h2" + ); + + Name.textContent = + Mutation.Name; + + + Header.append( + Number, + Name + ); + + + const Body = + document.createElement( + "div" + ); + + Body.className = + "MutationEncyclopediaBody"; + + + const Description = + document.createElement( + "p" + ); + + Description.className = + "MutationEncyclopediaDescription"; + + Description.textContent = + Mutation.Description || + "No description."; + + + Body.append( + Description, + CreateMutationEncyclopediaStats( + Mutation + ), + CreateMutationRecipeFlow( + Mutation + ), + CreateMutationEncyclopediaRelations( + Mutation + ) + ); + + + Card.append( + Header, + Body + ); + + + return Card; +} + + +function CreateMutationEncyclopediaStats( + Mutation +) { + const Stats = + document.createElement( + "div" + ); + + Stats.className = + "MutationEncyclopediaStats"; + + + Stats.append( + CreateMutationEncyclopediaStat( + "Chance", + FormatMutationChance( + Mutation.Chance + ) + ), + CreateMutationEncyclopediaStat( + "Cooldown", + FormatMutationEncyclopediaTime( + Mutation.Cooldown + ) + ), + CreateMutationEncyclopediaStat( + "Rotation", + Mutation.Rotation ?? + "None" + ), + CreateMutationEncyclopediaStat( + "Mature plants", + Mutation.AllowImmature === true + ? "Not required" + : "Required" + ) + ); + + + return Stats; +} + + +function CreateMutationEncyclopediaStat( + Name, + Value +) { + const Stat = + document.createElement( + "div" + ); + + Stat.className = + "MutationEncyclopediaStat"; + + + const NameElement = + document.createElement( + "span" + ); + + NameElement.className = + "MutationEncyclopediaStatName"; + + NameElement.textContent = + Name; + + + const ValueElement = + document.createElement( + "span" + ); + + ValueElement.textContent = + Value; + + + Stat.append( + NameElement, + ValueElement + ); + + + return Stat; +} + + +function CreateMutationRecipeFlow( + Mutation +) { + const Section = + document.createElement( + "section" + ); + + Section.className = + "MutationEncyclopediaRecipeSection"; + + + const Title = + document.createElement( + "h3" + ); + + Title.textContent = + "Recipe"; + + + const Pattern = + NormalizeMutationDisplayMatrix( + Mutation.Pattern, + null, + null, + "Any" + ); + + + if (Pattern.length === 0) { + const Missing = + document.createElement( + "p" + ); + + Missing.className = + "MutationEncyclopediaMissingRecipe"; + + Missing.textContent = + "No recipe data."; + + Section.append( + Title, + Missing + ); + + return Section; + } + + + const PatternWidth = + Pattern[0].length; + + const Success = + NormalizeMutationDisplayMatrix( + Mutation.Success, + PatternWidth, + Pattern.length, + "Keep" + ); + + + const Flow = + document.createElement( + "div" + ); + + Flow.className = + "MutationEncyclopediaRecipeFlow"; + + + const Input = + CreateMutationRecipePanel( + "Arrange", + Pattern, + "Pattern" + ); + + const Arrow = + document.createElement( + "span" + ); + + Arrow.className = + "MutationEncyclopediaRecipeArrow"; + + Arrow.setAttribute( + "aria-hidden", + "true" + ); + + Arrow.textContent = + "→"; + + + const Output = + CreateMutationRecipePanel( + "Result", + Success, + "Result" + ); + + + Flow.append( + Input, + Arrow, + Output + ); + + + Section.append( + Title, + Flow + ); + + + return Section; +} + + +function CreateMutationRecipePanel( + Heading, + Matrix, + Mode +) { + const Panel = + document.createElement( + "div" + ); + + Panel.className = + "MutationEncyclopediaRecipePanel"; + + + const Title = + document.createElement( + "h4" + ); + + Title.textContent = + Heading; + + + const Grid = + document.createElement( + "div" + ); + + Grid.className = + "GuideRecipe MutationRecipeGrid"; + + Grid.style.setProperty( + "--RecipeWidth", + String( + Matrix[0]?.length ?? 1 + ) + ); + + + const Labels = []; + + + for ( + const Row + of Matrix + ) { + for ( + const Value + of Row + ) { + const Cell = + CreateMutationRecipeCell( + Value, + Mode + ); + + Grid.appendChild( + Cell.Element + ); + + Labels.push( + Cell.Label + ); + } + } + + + Grid.setAttribute( + "aria-label", + Labels.join( + ", " + ) + ); + + + Panel.append( + Title, + Grid + ); + + + return Panel; +} + + +function CreateMutationRecipeCell( + Value, + Mode +) { + if (Mode === "Result") { + return CreateMutationResultCell( + Value + ); + } + + return CreateMutationPatternCell( + Value + ); +} + + +function CreateMutationPatternCell( + Matcher +) { + if ( + Matcher === null || + Matcher === "Any" + ) { + return CreateMutationTextCell( + "Any", + "MutationRecipeAny" + ); + } + + + if (Matcher === "Empty") { + return CreateMutationTextCell( + "Empty", + "GuideRecipeEmpty" + ); + } + + + if ( + typeof Matcher === + "string" + ) { + return CreateMutationPlantCell( + Matcher + ); + } + + + if ( + Matcher === null || + typeof Matcher !== + "object" + ) { + return CreateMutationTextCell( + "Any", + "MutationRecipeAny" + ); + } + + + if ( + typeof Matcher.Plant === + "string" + ) { + return CreateMutationPlantCell( + Matcher.Plant + ); + } + + + const Requirements = []; + + + if ( + Array.isArray( + Matcher.Tags + ) && + Matcher.Tags.length > 0 + ) { + Requirements.push( + Matcher.Tags.join( + " + " + ) + ); + } + + + if ( + Array.isArray( + Matcher.TagsAny + ) && + Matcher.TagsAny.length > 0 + ) { + Requirements.push( + "Any: " + + Matcher.TagsAny.join( + " / " + ) + ); + } + + + if ( + Array.isArray( + Matcher.TagsNot + ) && + Matcher.TagsNot.length > 0 + ) { + Requirements.push( + "Not: " + + Matcher.TagsNot.join( + ", " + ) + ); + } + + + if (Requirements.length === 0) { + return CreateMutationTextCell( + "Any", + "MutationRecipeAny" + ); + } + + + return CreateMutationTextCell( + Requirements.join( + "\n" + ), + "MutationRecipeMatcher" + ); +} + + +function CreateMutationResultCell( + Result +) { + if ( + Result === null || + Result === "Keep" + ) { + return CreateMutationTextCell( + "Keep", + "MutationRecipeKeep" + ); + } + + + if (Result === "Empty") { + return CreateMutationTextCell( + "Empty", + "GuideRecipeEmpty" + ); + } + + + if ( + typeof Result === + "string" + ) { + if ( + Result.startsWith( + "$" + ) + ) { + return CreateMutationTextCell( + Result.slice(1), + "MutationRecipeCapture" + ); + } + + return CreateMutationPlantCell( + Result + ); + } + + + if ( + Result !== null && + typeof Result === + "object" && + typeof Result.Plant === + "string" + ) { + if ( + Result.Plant.startsWith( + "$" + ) + ) { + return CreateMutationTextCell( + Result.Plant.slice(1), + "MutationRecipeCapture" + ); + } + + return CreateMutationPlantCell( + Result.Plant + ); + } + + + return CreateMutationTextCell( + "Keep", + "MutationRecipeKeep" + ); +} + + +function CreateMutationPlantCell( + PlantKey +) { + const Plant = + Plants[PlantKey]; + + + if (Plant === undefined) { + return CreateMutationTextCell( + PlantKey, + "MutationRecipeMatcher" + ); + } + + + const Element = + document.createElement( + "div" + ); + + Element.className = + "PlantTile GuideRecipeCell GuideRecipePlant"; + + + const Images = + PlantImages[PlantKey] ?? []; + + + if (Images.length > 0) { + const Image = + document.createElement( + "img" + ); + + Image.className = + "PlantSprite GuideRecipeImage"; + + Image.src = + Images[ + Images.length - 1 + ]; + + Image.alt = + ""; + + Element.appendChild( + Image + ); + } + + + const Label = + document.createElement( + "span" + ); + + Label.className = + "GuideRecipeLabel"; + + Label.textContent = + Plant.Name; + + + Element.appendChild( + Label + ); + + + return { + Element: Element, + Label: Plant.Name + }; +} + + +function CreateMutationTextCell( + Text, + ClassName +) { + const Element = + document.createElement( + "div" + ); + + Element.className = + "PlantTile GuideRecipeCell " + + ClassName; + + + const Label = + document.createElement( + "span" + ); + + Label.className = + "MutationRecipeText"; + + Label.textContent = + Text; + + + Element.appendChild( + Label + ); + + + return { + Element: Element, + Label: Text.replaceAll( + "\n", + " " + ) + }; +} + + +function NormalizeMutationDisplayMatrix( + Matrix, + Width = null, + Height = null, + FillValue = "Any" +) { + if (!Array.isArray(Matrix)) { + return []; + } + + + const TargetHeight = + Height ?? Matrix.length; + + const TargetWidth = + Width ?? Math.max( + 0, + ...Matrix.map( + Row => + Array.isArray(Row) + ? Row.length + : 0 + ) + ); + + + if ( + TargetWidth <= 0 || + TargetHeight <= 0 + ) { + return []; + } + + + const Normalized = []; + + + for ( + let Y = 0; + Y < TargetHeight; + Y++ + ) { + const SourceRow = + Array.isArray( + Matrix[Y] + ) + ? Matrix[Y] + : []; + + const Row = []; + + + for ( + let X = 0; + X < TargetWidth; + X++ + ) { + Row.push( + X < SourceRow.length + ? SourceRow[X] + : FillValue + ); + } + + + Normalized.push( + Row + ); + } + + + return Normalized; +} + + +function CreateMutationEncyclopediaRelations( + Mutation +) { + const Relations = + document.createElement( + "div" + ); + + Relations.className = + "MutationEncyclopediaRelations"; + + + Relations.append( + CreateMutationPlantRelationGroup( + "Plants used", + Mutation.Relations + ?.PlantsUsed ?? [] + ), + CreateMutationPlantRelationGroup( + "Plants created", + Mutation.Relations + ?.PlantsCreated ?? [] + ) + ); + + + return Relations; +} + + +function CreateMutationPlantRelationGroup( + Heading, + PlantIds +) { + const Group = + document.createElement( + "section" + ); + + Group.className = + "MutationEncyclopediaRelationGroup"; + + + const Title = + document.createElement( + "h3" + ); + + Title.textContent = + Heading; + + + const List = + document.createElement( + "ul" + ); + + List.className = + "MutationEncyclopediaRelationList"; + + + const SortedPlantIds = [ + ...new Set( + PlantIds.map( + PlantId => + Number(PlantId) + ) + ) + ].sort( + (A, B) => A - B + ); + + + if (SortedPlantIds.length === 0) { + const Empty = + document.createElement( + "li" + ); + + Empty.className = + "MutationEncyclopediaRelationEmpty"; + + Empty.textContent = + "None"; + + List.appendChild( + Empty + ); + } else { + for ( + const PlantId + of SortedPlantIds + ) { + List.appendChild( + CreateMutationPlantRelation( + PlantId + ) + ); + } + } + + + Group.append( + Title, + List + ); + + + return Group; +} + + +function CreateMutationPlantRelation( + PlantId +) { + const Item = + document.createElement( + "li" + ); + + + if ( + !HasDiscoveredPlant( + MutationEncyclopediaSave, + PlantId + ) + ) { + Item.className = + "MutationEncyclopediaRelationUnknown"; + + Item.textContent = + "????????"; + + return Item; + } + + + const Plant = + GetPlantById( + PlantId + ); + + + if (Plant === null) { + Item.textContent = + "Plant " + + PlantId; + + return Item; + } + + + const Link = + document.createElement( + "a" + ); + + Link.href = + "/Pages/Plants.html#Plant-" + + PlantId; + + Link.textContent = + Plant.Name; + + + Item.appendChild( + Link + ); + + + return Item; +} + + +function FormatMutationChance( + Chance +) { + const Percent = + Math.max( + 0, + Math.min( + Number( + Chance ?? 1 + ), + 1 + ) + ) * 100; + + + return Percent.toLocaleString( + undefined, + { + maximumFractionDigits: 2 + } + ) + "%"; +} + + +function FormatMutationEncyclopediaTime( + Milliseconds +) { + let TotalSeconds = + Math.max( + 0, + Math.ceil( + Number( + Milliseconds ?? 0 + ) / + 1000 + ) + ); + + + if (TotalSeconds === 0) { + return "None"; + } + + + const Hours = + Math.floor( + TotalSeconds / + 3600 + ); + + TotalSeconds %= 3600; + + const Minutes = + Math.floor( + TotalSeconds / + 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.push( + Seconds + "s" + ); + } + + + return Parts.join( + " " + ); +} + + +function SetMutationEncyclopediaMessage( + Message +) { + const Element = + document.getElementById( + "MutationEncyclopediaMessage" + ); + + + if (Element !== null) { + Element.textContent = + Message; + } +} + + +document.addEventListener( + "DOMContentLoaded", + StartMutationEncyclopedia +); diff --git a/Styles/Style.css b/Styles/Style.css index 0f2de74..c8dacc9 100644 --- a/Styles/Style.css +++ b/Styles/Style.css @@ -1225,7 +1225,7 @@ select { display: grid; grid-template-columns: repeat( - 3, + var(--RecipeWidth, 3), minmax(0, 5.75rem) ); gap: 0.75rem; @@ -1288,7 +1288,7 @@ select { .GuideRecipe { grid-template-columns: repeat( - 3, + var(--RecipeWidth, 3), minmax(0, 1fr) ); gap: 0.5rem; @@ -1475,3 +1475,246 @@ select { gap: 0.2rem; } } + + +/* + * Mutation encyclopedia + */ + +.MutationEncyclopediaList { + display: flex; + flex-direction: column; + gap: 1rem; + + margin-top: 1.5rem; +} + +.MutationEncyclopediaCard { + overflow: hidden; + + box-shadow: 3px 3px 0 var(--Crust); +} + +.MutationEncyclopediaHeader { + display: flex; + gap: 0.75rem; + + align-items: baseline; + + padding: 0.75rem 1rem; + + background-color: var(--Surface0); + border-bottom: 3px solid var(--Surface2); +} + +.MutationEncyclopediaHeader h2 { + margin: 0; + + color: var(--Mauve); +} + +.MutationEncyclopediaNumber { + flex: 0 0 auto; + + color: var(--Overlay2); + + font-size: 1.1rem; +} + +.MutationEncyclopediaBody { + padding: 1rem; +} + +.MutationEncyclopediaDescription { + margin-bottom: 1rem; +} + +.MutationEncyclopediaStats { + display: grid; + grid-template-columns: + repeat( + 2, + minmax(0, 1fr) + ); + gap: 0 1rem; + + margin-bottom: 1.25rem; +} + +.MutationEncyclopediaStat { + display: flex; + justify-content: space-between; + gap: 0.75rem; + + padding: 0.35rem 0; + + border-bottom: 2px solid var(--Surface0); +} + +.MutationEncyclopediaStatName { + flex: 0 0 auto; + + color: var(--Subtext1); +} + +.MutationEncyclopediaStat > :last-child { + min-width: 0; + + text-align: right; + overflow-wrap: anywhere; +} + +.MutationEncyclopediaRecipeSection { + margin-bottom: 1.25rem; + padding: 1rem; + + background-color: var(--Base); + border: 3px solid var(--Surface2); +} + +.MutationEncyclopediaRecipeSection > h3 { + margin-bottom: 0.75rem; + + color: var(--Peach); +} + +.MutationEncyclopediaRecipeFlow { + display: grid; + grid-template-columns: + minmax(0, 1fr) + auto + minmax(0, 1fr); + gap: 0.75rem; + + align-items: center; +} + +.MutationEncyclopediaRecipePanel { + min-width: 0; +} + +.MutationEncyclopediaRecipePanel h4 { + margin-bottom: 0.25rem; + + color: var(--Subtext1); + + font-size: 1rem; + text-align: center; +} + +.MutationEncyclopediaRecipePanel .GuideRecipe { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.MutationEncyclopediaRecipeArrow { + color: var(--Yellow); + + font-size: 2rem; +} + +.MutationRecipeAny { + color: var(--Overlay2); + background-color: var(--Mantle); + border-style: dashed; +} + +.MutationRecipeMatcher { + color: var(--Sky); + background-color: var(--Mantle); +} + +.MutationRecipeKeep { + color: var(--Subtext1); + background-color: var(--Surface0); +} + +.MutationRecipeCapture { + color: var(--Lavender); + background-color: var(--Surface0); +} + +.MutationRecipeText { + max-width: 100%; + + overflow-wrap: anywhere; + + font-size: 0.75rem; + line-height: 1.15; + white-space: pre-line; +} + +.MutationEncyclopediaMissingRecipe { + margin-bottom: 0; + + color: var(--Overlay1); +} + +.MutationEncyclopediaRelations { + display: grid; + grid-template-columns: + repeat( + 2, + minmax(0, 1fr) + ); + gap: 1rem; +} + +.MutationEncyclopediaRelationGroup { + padding: 0.75rem; + + background-color: var(--Base); + border: 3px solid var(--Surface2); +} + +.MutationEncyclopediaRelationGroup h3 { + margin-bottom: 0.5rem; + + color: var(--Peach); + + font-size: 1rem; +} + +.MutationEncyclopediaRelationList { + margin: 0; + padding-left: 1.25rem; +} + +.MutationEncyclopediaRelationList li { + margin-bottom: 0.35rem; +} + +.MutationEncyclopediaRelationList li:last-child { + margin-bottom: 0; +} + +.MutationEncyclopediaRelationUnknown, +.MutationEncyclopediaRelationEmpty { + color: var(--Overlay1); +} + +@media (max-width: 700px) { + .MutationEncyclopediaStats, + .MutationEncyclopediaRelations { + grid-template-columns: 1fr; + } + + .MutationEncyclopediaRecipeFlow { + grid-template-columns: 1fr; + } + + .MutationEncyclopediaRecipeArrow { + justify-self: center; + + transform: rotate(90deg); + } + + .MutationEncyclopediaStat { + flex-direction: column; + gap: 0.2rem; + } + + .MutationEncyclopediaStat > :last-child { + text-align: left; + } +}