Plots
diff --git a/Pages/Shop.php b/Pages/Shop.php
index 54ea479..0c9c2ec 100644
--- a/Pages/Shop.php
+++ b/Pages/Shop.php
@@ -47,8 +47,8 @@ $PageSection = "Shop";
Permanent upgrades
- Permanently expand your Garden or unlock
- extra information and quality-of-life features.
+ Expand your active Garden, buy additional Gardens,
+ or unlock information and quality-of-life features.
{
+ SwitchGarden(-1);
+ }
+ );
+
+ NextButton?.addEventListener(
+ "click",
+ () => {
+ SwitchGarden(1);
+ }
+ );
+
+ NameInput?.addEventListener(
+ "change",
+ async () => {
+ const GardenIndex =
+ GameSave.ActiveGardenIndex;
+
+ const GardenName =
+ RenameGarden(
+ GameSave,
+ GardenIndex,
+ NameInput.value
+ );
+
+ NameInput.value =
+ GardenName;
+
+ SetGardenMessage(
+ "Garden renamed to " +
+ GardenName +
+ "."
+ );
+
+ await SaveGame(
+ GameSave
+ );
+ }
+ );
+
+ NameInput?.addEventListener(
+ "keydown",
+ Event => {
+ if (Event.key !== "Enter") {
+ return;
+ }
+
+ Event.preventDefault();
+ NameInput.blur();
+ }
+ );
+}
+
+
+async function SwitchGarden(
+ Direction
+) {
+ const GardenCount =
+ GameSave.Gardens.length;
+
+ if (GardenCount <= 1) {
+ return;
+ }
+
+ const NewIndex =
+ GameSave.ActiveGardenIndex +
+ Direction;
+
+ if (
+ NewIndex < 0 ||
+ NewIndex >= GardenCount
+ ) {
+ return;
+ }
+
+ SetActiveGardenIndex(
+ GameSave,
+ NewIndex
+ );
+
+ const MutationResult =
+ CheckGardenMutations(
+ GameSave
+ );
+
+ RenderGame();
+
+ SetGardenMessage(
+ "Switched to " +
+ GameSave.Garden.Name +
+ "."
+ );
+
+ await SaveGame(
+ GameSave
+ );
+
+ if (MutationResult.Attempted) {
+ RenderGame();
+ }
+}
+
+
+function RenderGardenSelector() {
+ const PreviousButton =
+ document.getElementById(
+ "PreviousGardenButton"
+ );
+
+ const NextButton =
+ document.getElementById(
+ "NextGardenButton"
+ );
+
+ const NameInput =
+ document.getElementById(
+ "GardenNameInput"
+ );
+
+ if (
+ PreviousButton === null ||
+ NextButton === null ||
+ NameInput === null
+ ) {
+ return;
+ }
+
+ const GardenCount =
+ GameSave.Gardens.length;
+
+ const GardenIndex =
+ GameSave.ActiveGardenIndex;
+
+ PreviousButton.disabled =
+ GardenIndex <= 0;
+
+ NextButton.disabled =
+ GardenIndex >=
+ GardenCount - 1;
+
+ if (
+ document.activeElement !==
+ NameInput
+ ) {
+ NameInput.value =
+ GameSave.Garden.Name;
+ }
+
+ NameInput.title =
+ "Garden " +
+ (GardenIndex + 1) +
+ " of " +
+ GardenCount;
+}
+
+
+function CheckAllGardenMutations(
+ SaveData
+) {
+ const OriginalGardenIndex =
+ SaveData.ActiveGardenIndex;
+
+ const Result = {
+ Attempted: false,
+ Changed: false
+ };
+
+
+ for (
+ let GardenIndex = 0;
+ GardenIndex <
+ SaveData.Gardens.length;
+ GardenIndex++
+ ) {
+ SetActiveGardenIndex(
+ SaveData,
+ GardenIndex
+ );
+
+ const GardenResult =
+ CheckGardenMutations(
+ SaveData
+ );
+
+ Result.Attempted =
+ Result.Attempted ||
+ GardenResult.Attempted;
+
+ Result.Changed =
+ Result.Changed ||
+ GardenResult.Changed;
+ }
+
+
+ SetActiveGardenIndex(
+ SaveData,
+ OriginalGardenIndex
+ );
+
+ return Result;
+}
+
+
function BindGardenTools() {
const ShovelButton =
document.getElementById(
@@ -326,6 +550,25 @@ function RenderGarden() {
GameSave.Garden.Width
);
+ const GardenBorderColours = [
+ "var(--Lavender)",
+ "var(--Mauve)",
+ "var(--Pink)",
+ "var(--Blue)",
+ "var(--Teal)",
+ "var(--Peach)",
+ "var(--Yellow)",
+ "var(--Green)"
+ ];
+
+ GardenGrid.style.setProperty(
+ "--GardenBorderColour",
+ GardenBorderColours[
+ GameSave.ActiveGardenIndex %
+ GardenBorderColours.length
+ ]
+ );
+
GardenGrid.replaceChildren();
@@ -954,7 +1197,7 @@ async function GardenTick() {
try {
const MutationResult =
- CheckGardenMutations(
+ CheckAllGardenMutations(
GameSave
);
diff --git a/Scripts/Garden/Save.js b/Scripts/Garden/Save.js
index b577eec..d04063c 100644
--- a/Scripts/Garden/Save.js
+++ b/Scripts/Garden/Save.js
@@ -10,6 +10,357 @@ const StartingSeeds = {
};
+const DefaultGardenName = "Garden";
+const MaximumGardenNameLength = 32;
+const StartingGardenWidth = 3;
+const StartingGardenHeight = 3;
+
+
+function CreateGardenData(
+ Name = DefaultGardenName
+) {
+ return {
+ Name: NormalizeGardenName(
+ Name
+ ),
+ Width: StartingGardenWidth,
+ Height: StartingGardenHeight,
+ Plots: Array(
+ StartingGardenWidth *
+ StartingGardenHeight
+ ).fill(null),
+ MutationCooldowns: {}
+ };
+}
+
+
+function NormalizeGardenName(
+ Value
+) {
+ if (
+ typeof Value !== "string"
+ ) {
+ return DefaultGardenName;
+ }
+
+ const Name = Value
+ .trim()
+ .slice(
+ 0,
+ MaximumGardenNameLength
+ );
+
+ return Name.length > 0
+ ? Name
+ : DefaultGardenName;
+}
+
+
+function NormalizeGardenData(
+ Garden,
+ LegacyMutationCooldowns = null
+) {
+ if (
+ Garden === null ||
+ typeof Garden !== "object" ||
+ Array.isArray(Garden)
+ ) {
+ Garden = CreateGardenData();
+ }
+
+ Garden.Name = NormalizeGardenName(
+ Garden.Name
+ );
+
+ Garden.Width = Math.max(
+ 1,
+ Math.floor(
+ Number(
+ Garden.Width ??
+ StartingGardenWidth
+ ) || StartingGardenWidth
+ )
+ );
+
+ Garden.Height = Math.max(
+ 1,
+ Math.floor(
+ Number(
+ Garden.Height ??
+ StartingGardenHeight
+ ) || StartingGardenHeight
+ )
+ );
+
+ if (!Array.isArray(Garden.Plots)) {
+ Garden.Plots = [];
+ }
+
+
+ for (
+ let PlotIndex = 0;
+ PlotIndex < Garden.Plots.length;
+ PlotIndex++
+ ) {
+ const Plot =
+ Garden.Plots[PlotIndex];
+
+ if (Plot === null) {
+ continue;
+ }
+
+ if (
+ typeof Plot !== "object" ||
+ Array.isArray(Plot)
+ ) {
+ Garden.Plots[PlotIndex] = null;
+ continue;
+ }
+
+ if (
+ Plot.Plant === "Rose"
+ ) {
+ Plot.Plant = "RedRose";
+ }
+
+ if (
+ typeof Plot.Plant ===
+ "number"
+ ) {
+ const PlantKey =
+ ResolvePlantKeyById(
+ Plot.Plant
+ );
+
+ if (PlantKey !== null) {
+ Plot.Plant = PlantKey;
+ }
+ }
+
+ Plot.PlantedAt = Number(
+ Plot.PlantedAt ??
+ Date.now()
+ );
+
+ delete Plot.VisualVariant;
+ delete Plot.AddedTags;
+ }
+
+
+ const RequiredPlots =
+ Garden.Width *
+ Garden.Height;
+
+ while (
+ Garden.Plots.length <
+ RequiredPlots
+ ) {
+ Garden.Plots.push(null);
+ }
+
+ if (
+ Garden.Plots.length >
+ RequiredPlots
+ ) {
+ Garden.Plots.length =
+ RequiredPlots;
+ }
+
+
+ if (
+ Garden.MutationCooldowns ===
+ undefined &&
+ LegacyMutationCooldowns !== null &&
+ typeof LegacyMutationCooldowns ===
+ "object" &&
+ !Array.isArray(
+ LegacyMutationCooldowns
+ )
+ ) {
+ Garden.MutationCooldowns =
+ LegacyMutationCooldowns;
+ }
+
+ if (
+ Garden.MutationCooldowns === null ||
+ typeof Garden.MutationCooldowns !==
+ "object" ||
+ Array.isArray(
+ Garden.MutationCooldowns
+ )
+ ) {
+ Garden.MutationCooldowns = {};
+ }
+
+
+ return Garden;
+}
+
+
+function GetActiveGarden(
+ SaveData
+) {
+ if (
+ !Array.isArray(
+ SaveData.Gardens
+ ) ||
+ SaveData.Gardens.length === 0
+ ) {
+ SaveData.Gardens = [
+ CreateGardenData()
+ ];
+ }
+
+ const GardenIndex = Math.min(
+ SaveData.Gardens.length - 1,
+ Math.max(
+ 0,
+ Math.floor(
+ Number(
+ SaveData.ActiveGardenIndex ??
+ 0
+ ) || 0
+ )
+ )
+ );
+
+ SaveData.ActiveGardenIndex =
+ GardenIndex;
+
+ return SaveData.Gardens[
+ GardenIndex
+ ];
+}
+
+
+function SetActiveGardenIndex(
+ SaveData,
+ GardenIndex
+) {
+ if (
+ !Array.isArray(
+ SaveData.Gardens
+ ) ||
+ SaveData.Gardens.length === 0
+ ) {
+ return false;
+ }
+
+ const NormalizedIndex = Math.min(
+ SaveData.Gardens.length - 1,
+ Math.max(
+ 0,
+ Math.floor(
+ Number(
+ GardenIndex
+ ) || 0
+ )
+ )
+ );
+
+ const Changed =
+ SaveData.ActiveGardenIndex !==
+ NormalizedIndex;
+
+ SaveData.ActiveGardenIndex =
+ NormalizedIndex;
+
+ return Changed;
+}
+
+
+function RenameGarden(
+ SaveData,
+ GardenIndex,
+ Name
+) {
+ if (
+ !Array.isArray(
+ SaveData.Gardens
+ ) ||
+ SaveData.Gardens[GardenIndex] ===
+ undefined
+ ) {
+ return DefaultGardenName;
+ }
+
+ const NormalizedName =
+ NormalizeGardenName(
+ Name
+ );
+
+ SaveData.Gardens[
+ GardenIndex
+ ].Name = NormalizedName;
+
+ return NormalizedName;
+}
+
+
+function DefineActiveGardenAliases(
+ SaveData
+) {
+ delete SaveData.Garden;
+ delete SaveData.MutationCooldowns;
+
+ Object.defineProperty(
+ SaveData,
+ "Garden",
+ {
+ configurable: true,
+ enumerable: false,
+
+ get() {
+ return GetActiveGarden(
+ SaveData
+ );
+ },
+
+ set(Value) {
+ const GardenIndex =
+ SaveData.ActiveGardenIndex ??
+ 0;
+
+ SaveData.Gardens[
+ GardenIndex
+ ] = NormalizeGardenData(
+ Value
+ );
+ }
+ }
+ );
+
+ Object.defineProperty(
+ SaveData,
+ "MutationCooldowns",
+ {
+ configurable: true,
+ enumerable: false,
+
+ get() {
+ return GetActiveGarden(
+ SaveData
+ ).MutationCooldowns;
+ },
+
+ set(Value) {
+ const Garden =
+ GetActiveGarden(
+ SaveData
+ );
+
+ Garden.MutationCooldowns =
+ Value !== null &&
+ typeof Value === "object" &&
+ !Array.isArray(Value)
+ ? Value
+ : {};
+ }
+ }
+ );
+}
+
+
function CreateNewSave() {
const SeedInventory = {};
const StartingPlants = [];
@@ -40,8 +391,8 @@ function CreateNewSave() {
}
- return {
- Version: 2,
+ return NormalizeSaveData({
+ Version: 3,
Revision: 0,
LastSavedAt: Date.now(),
@@ -83,19 +434,17 @@ function CreateNewSave() {
ShowReadyPlots: true
},
- Garden: {
- Width: 3,
- Height: 3,
- Plots: Array(9).fill(null)
- },
+ Gardens: [
+ CreateGardenData()
+ ],
+
+ ActiveGardenIndex: 0,
Discoveries: {
Plants: StartingPlants,
Mutations: []
- },
-
- MutationCooldowns: {}
- };
+ }
+ });
}
@@ -106,7 +455,7 @@ function NormalizeSaveData(
Number(
SaveData.Version ?? 1
),
- 2
+ 3
);
SaveData.Revision = Number(
@@ -301,100 +650,48 @@ function NormalizeSaveData(
false;
- SaveData.Garden ??= {
- Width: 3,
- Height: 3,
- Plots: []
- };
+ const LegacyGarden =
+ SaveData.Garden ??
+ null;
- SaveData.Garden.Width = Math.max(
- 1,
- Math.floor(
- Number(
- SaveData.Garden.Width ?? 3
- ) || 3
- )
- );
+ const LegacyMutationCooldowns =
+ SaveData.MutationCooldowns ??
+ null;
- SaveData.Garden.Height = Math.max(
- 1,
- Math.floor(
- Number(
- SaveData.Garden.Height ?? 3
- ) || 3
- )
- );
-
- SaveData.Garden.Plots ??= [];
-
-
- for (
- let PlotIndex = 0;
- PlotIndex <
- SaveData.Garden.Plots.length;
- PlotIndex++
- ) {
- const Plot =
- SaveData.Garden.Plots[
- PlotIndex
- ];
-
- if (Plot === null) {
- continue;
- }
-
- if (
- Plot.Plant === "Rose"
- ) {
- Plot.Plant =
- "RedRose";
- }
-
- if (
- typeof Plot.Plant ===
- "number"
- ) {
- const PlantKey =
- ResolvePlantKeyById(
- Plot.Plant
- );
-
- if (PlantKey !== null) {
- Plot.Plant = PlantKey;
- }
- }
-
- Plot.PlantedAt = Number(
- Plot.PlantedAt ??
- Date.now()
- );
-
- delete Plot.VisualVariant;
- delete Plot.AddedTags;
- }
-
-
- const RequiredPlots =
- SaveData.Garden.Width *
- SaveData.Garden.Height;
-
- while (
- SaveData.Garden.Plots.length <
- RequiredPlots
- ) {
- SaveData.Garden.Plots.push(null);
- }
if (
- SaveData.Garden.Plots.length >
- RequiredPlots
+ !Array.isArray(
+ SaveData.Gardens
+ ) ||
+ SaveData.Gardens.length === 0
) {
- SaveData.Garden.Plots.length =
- RequiredPlots;
+ SaveData.Gardens = [
+ LegacyGarden ??
+ CreateGardenData()
+ ];
}
- SaveData.MutationCooldowns ??= {};
+ SaveData.Gardens =
+ SaveData.Gardens.map(
+ (Garden, GardenIndex) =>
+ NormalizeGardenData(
+ Garden,
+ GardenIndex === 0
+ ? LegacyMutationCooldowns
+ : null
+ )
+ );
+
+
+ SetActiveGardenIndex(
+ SaveData,
+ SaveData.ActiveGardenIndex ?? 0
+ );
+
+ DefineActiveGardenAliases(
+ SaveData
+ );
return SaveData;
diff --git a/Scripts/Garden/Shop.js b/Scripts/Garden/Shop.js
index 23ac82e..dd66dc5 100644
--- a/Scripts/Garden/Shop.js
+++ b/Scripts/Garden/Shop.js
@@ -124,6 +124,7 @@ function RenderGardenUpgrades() {
CreateGardenExpansionCard(
"Row"
),
+ CreateNewGardenCard(),
CreatePlantInformationUpgradeCard(),
CreateGardenOverviewUpgradeCard(),
CreateMutationHintsUpgradeCard()
@@ -210,6 +211,10 @@ function CreateGardenExpansionCard(
"ShopItemDetails";
Details.append(
+ CreateShopStat(
+ "Garden",
+ ShopSave.Garden.Name
+ ),
CreateShopStat(
"Current size",
ShopSave.Garden.Width +
@@ -273,6 +278,115 @@ function CreateGardenExpansionCard(
}
+function CreateNewGardenCard() {
+ const Cost =
+ GetNewGardenCost(
+ ShopSave
+ );
+
+ const GardensOwned =
+ ShopSave.Gardens.length;
+
+
+ const Card =
+ document.createElement(
+ "article"
+ );
+
+ Card.className =
+ "Panel ShopItem";
+
+
+ const Header =
+ document.createElement(
+ "div"
+ );
+
+ Header.className =
+ "ShopItemHeader";
+
+
+ const Name =
+ document.createElement(
+ "h3"
+ );
+
+ Name.textContent =
+ "Buy new Garden";
+
+ Header.appendChild(Name);
+
+
+ const Description =
+ document.createElement(
+ "p"
+ );
+
+ Description.textContent =
+ "Buy another independent 3×3 Garden. New Gardens can be named and expanded separately.";
+
+
+ const Details =
+ document.createElement(
+ "div"
+ );
+
+ Details.className =
+ "ShopItemDetails";
+
+ Details.append(
+ CreateShopStat(
+ "Gardens owned",
+ GardensOwned.toLocaleString()
+ ),
+ CreateShopStat(
+ "New Garden size",
+ "3×3 (9 plots)"
+ ),
+ CreateShopStat(
+ "Price scaling",
+ "+50% per Garden"
+ )
+ );
+
+
+ const BuyButton =
+ document.createElement(
+ "button"
+ );
+
+ BuyButton.className =
+ "ActionButton ShopBuyButton";
+
+ BuyButton.type = "button";
+
+ BuyButton.textContent =
+ "Buy - " +
+ Cost.toLocaleString() +
+ " Dew";
+
+ BuyButton.disabled =
+ ShopSave.Currency.Dew < Cost ||
+ ShopPurchasePending;
+
+ BuyButton.addEventListener(
+ "click",
+ BuyNewGarden
+ );
+
+
+ Card.append(
+ Header,
+ Description,
+ Details,
+ BuyButton
+ );
+
+
+ return Card;
+}
+
+
function CreatePlantInformationUpgradeCard() {
const Cost =
GetPlantInformationUpgradeCost();
@@ -1188,6 +1302,60 @@ async function BuyGardenExpansion(
}
+async function BuyNewGarden() {
+ if (ShopPurchasePending) {
+ return;
+ }
+
+
+ const Cost =
+ GetNewGardenCost(
+ ShopSave
+ );
+
+ if (
+ ShopSave.Currency.Dew <
+ Cost
+ ) {
+ SetShopMessage(
+ "You don't have enough Dew for a new Garden."
+ );
+
+ return;
+ }
+
+
+ ShopPurchasePending = true;
+
+ try {
+ ShopSave.Currency.Dew -=
+ Cost;
+
+ ShopSave.Statistics.CurrencySpent.Dew +=
+ Cost;
+
+ AddNewGarden(
+ ShopSave
+ );
+
+ SetShopMessage(
+ "Bought a new 3×3 Garden for " +
+ Cost.toLocaleString() +
+ " Dew. It is now your active Garden."
+ );
+
+ RenderShop();
+
+ await SaveGame(
+ ShopSave
+ );
+ } finally {
+ ShopPurchasePending = false;
+ RenderShop();
+ }
+}
+
+
async function BuyPlantInformationUpgrade() {
if (
ShopPurchasePending ||
diff --git a/Scripts/Garden/Upgrades.js b/Scripts/Garden/Upgrades.js
index f725421..5537527 100644
--- a/Scripts/Garden/Upgrades.js
+++ b/Scripts/Garden/Upgrades.js
@@ -2,6 +2,9 @@ const GardenExpansionBaseCost = 1000;
const PlantInformationUpgradeCost = 500;
const GardenOverviewUpgradeCost = 1000;
const MutationHintsUpgradeCost = 1500;
+const NewGardenBaseCost = 30000;
+const NewGardenCostMultiplier = 1.5;
+const NewGardenCostRounding = 1000;
function GetGardenColumnUpgradeCost(
@@ -112,6 +115,51 @@ function ResetGardenMutationCooldowns(
}
+function GetNewGardenCost(
+ SaveData
+) {
+ const GardensOwned = Math.max(
+ 1,
+ SaveData.Gardens?.length ?? 1
+ );
+
+ let Cost =
+ NewGardenBaseCost;
+
+ for (
+ let GardenNumber = 1;
+ GardenNumber < GardensOwned;
+ GardenNumber++
+ ) {
+ Cost = Math.round(
+ Cost *
+ NewGardenCostMultiplier /
+ NewGardenCostRounding
+ ) * NewGardenCostRounding;
+ }
+
+ return Cost;
+}
+
+
+function AddNewGarden(
+ SaveData
+) {
+ SaveData.Gardens ??= [];
+
+ SaveData.Gardens.push(
+ CreateGardenData()
+ );
+
+ SetActiveGardenIndex(
+ SaveData,
+ SaveData.Gardens.length - 1
+ );
+
+ return SaveData.Garden;
+}
+
+
function HasPlantInformationUpgrade(
SaveData
) {
diff --git a/Styles/Style.css b/Styles/Style.css
index 8599e77..887b08b 100644
--- a/Styles/Style.css
+++ b/Styles/Style.css
@@ -313,6 +313,45 @@ input, button, textarea, select {
image-rendering: auto;
}
+.GardenSelectorPanel {
+ display: grid;
+ grid-template-columns: 3rem minmax(0, 1fr) 3rem;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+ padding: 0.5rem;
+ background-color: var(--Mantle);
+ border: 3px solid var(--Surface2);
+}
+
+.GardenSelectorButton {
+ width: 100%;
+ min-width: 0;
+ padding-right: 0;
+ padding-left: 0;
+ font-size: 1.25rem;
+}
+
+.GardenNameInput {
+ width: 100%;
+ min-width: 0;
+ padding: 0.6rem 0.75rem;
+ color: var(--Text);
+ background-color: var(--Surface0);
+ border: 3px solid var(--Surface2);
+ border-radius: 0;
+ text-align: center;
+ box-shadow: 3px 3px 0 var(--Crust);
+}
+
+.GardenNameInput:hover {
+ border-color: var(--Sky);
+}
+
+.GardenNameInput:focus {
+ outline: 3px solid var(--Yellow);
+ outline-offset: 3px;
+}
+
.GardenLayout {
display: grid;
grid-template-columns: minmax(0, 1fr) 12rem;
@@ -327,7 +366,7 @@ input, button, textarea, select {
gap: 0.5rem;
padding: 0.75rem;
background-color: var(--Mantle);
- border: 4px solid var(--Lavender);
+ border: 4px solid var(--GardenBorderColour, var(--Lavender));
}
.GardenPlot {