Files
srhtnin.github.io/Scripts/Garden/Garden.js
T

3415 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
let GameSave;
let SelectedSeedId = null;
let SelectedGardenTool = "Trowel";
let FutureSightPreview = null;
let MutationCheckPending = false;
let QuickBuyPurchasePending = false;
let QuickBuyAmountMode = "One";
const GardenUpdateInterval = 1000;
const GardenBorderColours = [
"var(--Rosewater)",
"var(--Flamingo)",
"var(--Pink)",
"var(--Mauve)",
"var(--Red)",
"var(--Maroon)",
"var(--Peach)",
"var(--Yellow)",
"var(--Green)",
"var(--Teal)",
"var(--Sky)",
"var(--Sapphire)",
"var(--Blue)",
"var(--Lavender)"
];
const GardenPlotDefaultRotation = "East";
const GardenPlotRotations = [
"North",
"East",
"South",
"West"
];
const GardenPlotRotationLabels = {
North: "^",
East: "->",
South: "v",
West: "<-"
};
function GetGardenBorderColour(
GardenIndex
) {
const ColourCount =
GardenBorderColours.length;
const NormalizedIndex =
(
Number(GardenIndex) %
ColourCount +
ColourCount
) % ColourCount;
return GardenBorderColours[
NormalizedIndex
];
}
async function StartGame() {
try {
await LoadGameContent();
} catch (Error) {
console.error(
"Couldn't load Garden content:",
Error
);
SetGardenMessage(
"Couldn't load plant or mutation data."
);
return;
}
GameSave = await LoadGame();
const InitialSimulationResult =
AdvanceGameSimulation(
GameSave,
Date.now()
);
BindGardenTools();
BindGardenSelector();
BindQuickBuyControls();
EnsureSelectedSeed();
RenderGame();
if (
InitialSimulationResult.Advanced ||
InitialSimulationResult.Dirty
) {
await SaveGame(
GameSave
);
}
setInterval(
GardenTick,
GardenUpdateInterval
);
await RenderUser();
}
function RenderGame() {
EnsureSelectedGardenTool();
EnsureSelectedSeed();
RenderCurrency();
RenderQuickBuy();
RenderSeeds();
RenderTools();
RenderGardenSelector();
RenderGarden();
RenderNextHarvest();
RenderGardenOverview();
RenderGardenEconomy();
RenderGardenInfoLayout();
}
function BindGardenSelector() {
const PreviousButton =
document.getElementById(
"PreviousGardenButton"
);
const NextButton =
document.getElementById(
"NextGardenButton"
);
const NameInput =
document.getElementById(
"GardenNameInput"
);
PreviousButton?.addEventListener(
"click",
() => {
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 ActionTime = Date.now();
AdvanceGameSimulation(
GameSave,
ActionTime
);
const GardenCount =
GameSave.Gardens.length;
if (GardenCount <= 1) {
return;
}
const NewIndex =
GameSave.ActiveGardenIndex +
Direction;
if (
NewIndex < 0 ||
NewIndex >= GardenCount
) {
return;
}
SetActiveGardenIndex(
GameSave,
NewIndex
);
FutureSightPreview = null;
if (
SelectedGardenTool ===
"FutureSight"
) {
SelectedGardenTool =
"Trowel";
}
const MutationResult =
CheckGardenMutations(
GameSave,
ActionTime
);
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;
const HasPreviousGarden =
GardenIndex > 0;
const HasNextGarden =
GardenIndex <
GardenCount - 1;
PreviousButton.disabled =
!HasPreviousGarden;
NextButton.disabled =
!HasNextGarden;
PreviousButton.style.setProperty(
"--GardenSelectorBorderColour",
HasPreviousGarden
? GetGardenBorderColour(
GardenIndex - 1
)
: "var(--Surface2)"
);
NameInput.style.setProperty(
"--GardenSelectorBorderColour",
GetGardenBorderColour(
GardenIndex
)
);
NextButton.style.setProperty(
"--GardenSelectorBorderColour",
HasNextGarden
? GetGardenBorderColour(
GardenIndex + 1
)
: "var(--Surface2)"
);
PreviousButton.setAttribute(
"aria-label",
HasPreviousGarden
? "Previous Garden: " +
GameSave.Gardens[
GardenIndex - 1
].Name
: "No previous Garden"
);
NextButton.setAttribute(
"aria-label",
HasNextGarden
? "Next Garden: " +
GameSave.Gardens[
GardenIndex + 1
].Name
: "No next Garden"
);
if (
document.activeElement !==
NameInput
) {
NameInput.value =
GameSave.Garden.Name;
}
NameInput.title =
"Garden " +
(GardenIndex + 1) +
" of " +
GardenCount;
}
function CheckAllGardenMutations(
SaveData,
AtTime = GetSimulationTime(
SaveData
)
) {
return CheckAllGardenMutationsAtTime(
SaveData,
AtTime
);
}
function BindGardenTools() {
const ToolList =
document.getElementById(
"ToolList"
);
if (ToolList === null) {
return;
}
ToolList.addEventListener(
"click",
Event => {
const Button =
Event.target.closest(
"[data-garden-tool]"
);
if (
Button === null ||
!ToolList.contains(
Button
)
) {
return;
}
const Tool =
Button.dataset
.gardenTool;
SelectGardenTool(
Tool
);
}
);
}
async function SelectGardenTool(
Tool
) {
if (
!IsGardenToolUnlocked(
GameSave,
Tool
)
) {
return;
}
if (Tool === "FutureSight") {
if (
!IsFutureSightReady(
GameSave
) &&
FutureSightPreview === null
) {
SetGardenMessage(
"Future Sight is still recharging for " +
FormatGardenRemainingTime(
GetFutureSightCooldownRemaining(
GameSave
)
) +
"."
);
RenderTools();
return;
}
SelectedGardenTool =
"FutureSight";
if (FutureSightPreview === null) {
await UseFutureSight();
} else {
RenderTools();
RenderGarden();
}
return;
}
SelectedGardenTool = Tool;
FutureSightPreview = null;
RenderSeeds();
RenderTools();
RenderGarden();
}
function EnsureSelectedGardenTool() {
if (
!IsGardenToolUnlocked(
GameSave,
SelectedGardenTool
)
) {
SelectedGardenTool =
"Trowel";
FutureSightPreview = null;
}
}
function RenderTools() {
const ToolList =
document.getElementById(
"ToolList"
);
if (ToolList === null) {
return;
}
EnsureSelectedGardenTool();
const ToolDefinitions = [
[
"Trowel",
"Trowel"
],
[
"MagicTrowel",
"Magic trowel"
],
[
"Shovel",
"Shovel"
],
[
"Fertilizer",
"Fertilizer"
],
[
"FutureSight",
"Future Sight"
]
];
ToolList.replaceChildren();
for (
const [Tool, Name]
of ToolDefinitions
) {
if (
!IsGardenToolUnlocked(
GameSave,
Tool
)
) {
continue;
}
const Button =
document.createElement(
"button"
);
Button.className =
"ActionButton ToolButton ToolButton" +
Tool;
Button.type = "button";
Button.dataset.gardenTool =
Tool;
let Label = Name;
if (Tool === "Fertilizer") {
Label +=
" ×" +
Number(
GameSave.Inventory
.Fertilizer ?? 0
).toLocaleString();
}
if (Tool === "FutureSight") {
const Remaining =
GetFutureSightCooldownRemaining(
GameSave
);
if (Remaining > 0) {
Label +=
" (" +
FormatGardenRemainingTime(
Remaining
) +
")";
}
Button.disabled =
Remaining > 0 &&
!(
SelectedGardenTool ===
"FutureSight" &&
FutureSightPreview !==
null
);
}
Button.textContent = Label;
if (
SelectedGardenTool ===
Tool
) {
Button.setAttribute(
"aria-current",
"true"
);
}
ToolList.appendChild(
Button
);
}
}
function BindQuickBuyControls() {
const Controls = [
[
"QuickBuyOneButton",
"One"
],
[
"QuickBuyTenButton",
"Ten"
],
[
"QuickBuyGardenButton",
"Garden"
]
];
for (
const [ButtonId, Mode]
of Controls
) {
const Button =
document.getElementById(
ButtonId
);
Button?.addEventListener(
"click",
() => {
QuickBuyAmountMode =
Mode;
RenderQuickBuy();
}
);
}
}
function GetQuickBuyAmount() {
if (
QuickBuyAmountMode ===
"Ten"
) {
return 10;
}
if (
QuickBuyAmountMode ===
"Garden"
) {
return Math.max(
1,
GameSave.Garden.Width *
GameSave.Garden.Height
);
}
return 1;
}
function RenderQuickBuy() {
const PlantList =
document.getElementById(
"QuickBuyPlantList"
);
if (PlantList === null) {
return;
}
const Amount =
GetQuickBuyAmount();
const OneButton =
document.getElementById(
"QuickBuyOneButton"
);
const TenButton =
document.getElementById(
"QuickBuyTenButton"
);
const GardenButton =
document.getElementById(
"QuickBuyGardenButton"
);
const AmountButtons = [
[OneButton, "One", 1],
[TenButton, "Ten", 10],
[
GardenButton,
"Garden",
Math.max(
1,
GameSave.Garden.Width *
GameSave.Garden.Height
)
]
];
for (
const [Button, Mode, ButtonAmount]
of AmountButtons
) {
if (Button === null) {
continue;
}
Button.textContent =
"×" +
ButtonAmount.toLocaleString();
Button.disabled =
QuickBuyPurchasePending;
if (
QuickBuyAmountMode ===
Mode
) {
Button.setAttribute(
"aria-current",
"true"
);
} else {
Button.removeAttribute(
"aria-current"
);
}
}
PlantList.replaceChildren();
const AvailablePlants =
Object.values(Plants)
.filter(
Plant => {
if (
!IsPlantAvailableInShop(
GameSave,
Plant
)
) {
return false;
}
const Cost =
GetPlantShopCost(
GameSave,
Plant.Id
);
return (
Cost !== null &&
GameSave.Currency.Dew >=
Cost * Amount
);
}
)
.sort(
(A, B) =>
A.Id - B.Id
);
if (
AvailablePlants.length === 0
) {
const EmptyMessage =
document.createElement(
"p"
);
EmptyMessage.className =
"QuickBuyEmpty";
EmptyMessage.textContent =
Amount === 1
? "You can't afford any available seeds."
: "You can't afford this many of any available seed.";
PlantList.appendChild(
EmptyMessage
);
return;
}
for (
const Plant
of AvailablePlants
) {
PlantList.appendChild(
CreateQuickBuyPlantButton(
Plant,
Amount
)
);
}
}
function CreateQuickBuyPlantButton(
Plant,
Amount
) {
const UnitCost =
GetPlantShopCost(
GameSave,
Plant.Id
);
const TotalCost =
UnitCost * Amount;
const Button =
document.createElement(
"button"
);
Button.className =
"PlantTile QuickBuyPlantButton";
Button.type = "button";
Button.disabled =
QuickBuyPurchasePending;
Button.title =
"Buy " +
Amount.toLocaleString() +
(Amount === 1
? " seed of "
: " seeds of ") +
Plant.Name +
" for " +
TotalCost.toLocaleString() +
" Dew";
const Name =
document.createElement(
"span"
);
Name.className =
"QuickBuyPlantName";
Name.textContent =
Plant.Name;
const Visual =
document.createElement(
"span"
);
Visual.className =
"QuickBuyPlantVisual";
const MatureImage =
GetPlantMatureImageSource(
Plant
);
if (MatureImage === null) {
const Missing =
document.createElement(
"span"
);
Missing.className =
"QuickBuyPlantMissing";
Missing.textContent =
"No image";
Visual.appendChild(
Missing
);
} else {
const Image =
document.createElement(
"img"
);
Image.className =
"PlantSprite";
Image.src =
MatureImage;
Image.alt =
Plant.Name;
Visual.appendChild(
Image
);
}
Button.append(
Name,
Visual
);
Button.addEventListener(
"click",
() => {
BuyQuickBuyPlantSeed(
Plant.Id
);
}
);
return Button;
}
async function BuyQuickBuyPlantSeed(
PlantId
) {
if (QuickBuyPurchasePending) {
return;
}
const Plant =
GetPlantById(
PlantId
);
if (
Plant === null ||
!IsPlantAvailableInShop(
GameSave,
Plant
)
) {
return;
}
const Amount =
GetQuickBuyAmount();
const UnitCost =
GetPlantShopCost(
GameSave,
PlantId
);
if (UnitCost === null) {
SetGardenMessage(
"That seed doesn't have a purchasable recipe yet."
);
return;
}
const TotalCost =
UnitCost * Amount;
if (
GameSave.Currency.Dew <
TotalCost
) {
SetGardenMessage(
"You don't have enough Dew for " +
Amount.toLocaleString() +
(Amount === 1
? " seed of "
: " seeds of ") +
Plant.Name +
"."
);
RenderQuickBuy();
return;
}
QuickBuyPurchasePending = true;
try {
GameSave.Currency.Dew -=
TotalCost;
GameSave.Statistics.CurrencySpent.Dew +=
TotalCost;
GameSave.Statistics.SeedsPurchased +=
Amount;
AddSeed(
GameSave,
PlantId,
Amount
);
DiscoverPlant(
GameSave,
PlantId
);
if (
GameSave.Preferences
.SelectQuickBoughtPlant !==
false
) {
SelectInventoryPlant(
PlantId
);
}
SetGardenMessage(
"Bought " +
Amount.toLocaleString() +
" " +
Plant.Name +
(Amount === 1
? " seed for "
: " seeds for ") +
TotalCost.toLocaleString() +
" Dew."
);
RenderGame();
await SaveGame(
GameSave
);
} finally {
QuickBuyPurchasePending =
false;
RenderGame();
}
}
function RenderCurrency() {
const DewAmount =
document.getElementById(
"DewAmount"
);
DewAmount.textContent =
GameSave.Currency.Dew
.toLocaleString();
}
function EnsureSelectedSeed() {
if (
SelectedSeedId !== null &&
GetSeedCount(
GameSave,
SelectedSeedId
) > 0
) {
return;
}
SelectedSeedId = null;
const SeedIds = Object.keys(
GameSave.Inventory.Seeds
)
.map(Number)
.filter(
PlantId =>
GetSeedCount(
GameSave,
PlantId
) > 0 &&
GetPlantById(
PlantId
) !== null
)
.sort(
(A, B) => A - B
);
if (SeedIds.length > 0) {
SelectedSeedId =
SeedIds[0];
return;
}
}
function SelectInventoryPlant(
PlantId
) {
if (
GetPlantById(
PlantId
) === null ||
GetSeedCount(
GameSave,
PlantId
) <= 0
) {
return false;
}
SelectedSeedId =
Number(PlantId);
if (
GameSave.Preferences
.SelectTrowelWithInventoryPlant !==
false
) {
SelectedGardenTool =
"Trowel";
FutureSightPreview = null;
}
return true;
}
function RenderSeeds() {
const SeedList =
document.getElementById(
"SeedList"
);
SeedList.replaceChildren();
const SeedIds = Object.keys(
GameSave.Inventory.Seeds
)
.map(Number)
.filter(
PlantId =>
GetSeedCount(
GameSave,
PlantId
) > 0 &&
GetPlantById(
PlantId
) !== null
)
.sort(
(A, B) => A - B
);
if (SeedIds.length === 0) {
const EmptyMessage =
document.createElement(
"p"
);
EmptyMessage.className =
"SeedEmpty";
EmptyMessage.append(
"You don't have any seeds. Visit the "
);
const ShopLink =
document.createElement(
"a"
);
ShopLink.href =
"/Pages/Shop.html";
ShopLink.textContent =
"shop";
EmptyMessage.append(
ShopLink,
"."
);
SeedList.appendChild(
EmptyMessage
);
return;
}
for (
const PlantId
of SeedIds
) {
const Plant =
GetPlantById(
PlantId
);
if (Plant === null) {
continue;
}
const Button =
document.createElement(
"button"
);
Button.className =
"ActionButton SeedButton";
Button.type = "button";
Button.textContent =
Plant.Name +
" ×" +
GetSeedCount(
GameSave,
PlantId
);
if (
PlantId ===
SelectedSeedId
) {
Button.setAttribute(
"aria-current",
"true"
);
}
Button.addEventListener(
"click",
() => {
SelectInventoryPlant(
PlantId
);
RenderSeeds();
RenderTools();
RenderGarden();
SetGardenMessage(
"Selected " +
Plant.Name +
"."
);
}
);
SeedList.appendChild(
Button
);
}
}
function RenderGarden() {
const GardenGrid =
document.getElementById(
"GardenGrid"
);
const GardenPanel =
document.getElementById(
"Plots"
);
GardenGrid.style.setProperty(
"--GardenWidth",
GameSave.Garden.Width
);
GardenPanel.style.setProperty(
"--GardenBorderColour",
GetGardenBorderColour(
GameSave.ActiveGardenIndex
)
);
GardenPanel.classList.toggle(
"GardenPlotsFutureSight",
SelectedGardenTool ===
"FutureSight" &&
FutureSightPreview !== null
);
GardenGrid.replaceChildren();
const DisplaySettings =
GetGardenDisplaySettings();
const IsFutureSightPreview =
SelectedGardenTool ===
"FutureSight" &&
FutureSightPreview !== null;
GameSave.Garden.Plots.forEach(
(Plot, PlotIndex) => {
const DisplayPlot =
IsFutureSightPreview
? FutureSightPreview
.Plots[PlotIndex]
: Plot;
const PlotElement =
CreatePlotElement(
DisplayPlot,
PlotIndex,
DisplaySettings,
IsFutureSightPreview
);
if (IsFutureSightPreview) {
PlotElement.classList.add(
"GardenPlotFutureSight"
);
if (
!AreGardenPlotsEqual(
Plot,
DisplayPlot
)
) {
PlotElement.classList.add(
"GardenPlotFutureSightChanged"
);
}
}
GardenGrid.appendChild(
PlotElement
);
}
);
}
function AreGardenPlotsEqual(
A,
B
) {
if (A === null || B === null) {
return A === B;
}
return (
A.Plant === B.Plant &&
Number(A.PlantedAt) ===
Number(B.PlantedAt) &&
GetGardenPlotRotation(A) ===
GetGardenPlotRotation(B)
);
}
function GetGardenDisplaySettings() {
const HasPlantInformation =
HasPlantInformationUpgrade(
GameSave
);
return {
ShowPlantNames:
HasPlantInformation &&
GameSave.Preferences
.ShowPlantNames !==
false,
ShowGrowthTimers:
HasPlantInformation &&
GameSave.Preferences
.ShowGrowthTimers !==
false,
ShowPlotRotation:
HasRotationUpgrade(
GameSave
) &&
GameSave.Preferences
.ShowPlotRotation !==
false
};
}
function GetGardenPlotRotation(
Plot
) {
return GardenPlotRotations.includes(
Plot?.Rotation
)
? Plot.Rotation
: GardenPlotDefaultRotation;
}
function GetGardenPlotRotationLabel(
Plot
) {
return GardenPlotRotationLabels[
GetGardenPlotRotation(
Plot
)
] ?? "->";
}
async function RotateGardenPlot(
PlotIndex
) {
if (
!HasRotationUpgrade(
GameSave
)
) {
return;
}
const ActionTime = Date.now();
AdvanceGameSimulation(
GameSave,
ActionTime
);
const Plot =
GameSave.Garden.Plots[
PlotIndex
];
if (
Plot === null ||
typeof Plot !== "object"
) {
return;
}
const CurrentIndex =
GardenPlotRotations.indexOf(
GetGardenPlotRotation(
Plot
)
);
Plot.Rotation =
GardenPlotRotations[
(CurrentIndex + 1) %
GardenPlotRotations.length
];
const Plant =
Plants[Plot.Plant];
SetGardenMessage(
"Rotated " +
(Plant?.Name ?? "plot") +
" " +
Plot.Rotation.toLowerCase() +
"."
);
RenderGame();
await SaveGame(
GameSave
);
}
function CreatePlotElement(
Plot,
PlotIndex,
DisplaySettings,
IsPreview = false
) {
const Tile =
document.createElement(
"div"
);
Tile.className =
"PlantTile GardenPlot";
const MainButton =
document.createElement(
"button"
);
MainButton.className =
"GardenPlotMain";
MainButton.type = "button";
if (
DisplaySettings.ShowPlantNames
) {
Tile.classList.add(
"GardenPlotWithName"
);
}
const Plant =
Plot === null
? null
: Plants[Plot.Plant];
if (
DisplaySettings.ShowPlantNames
) {
MainButton.appendChild(
CreateGardenPlotName(
Plot,
Plant
)
);
}
const Visual =
document.createElement(
"span"
);
Visual.className =
"GardenPlotVisual";
MainButton.appendChild(
Visual
);
Tile.appendChild(
MainButton
);
if (Plot === null) {
Tile.classList.add(
"GardenPlotEmpty"
);
MainButton.title =
IsPreview
? "Future Sight: empty plot"
: "Empty plot";
AppendGardenPlotFooter(
Tile,
null,
null,
0,
PlotIndex,
DisplaySettings,
IsPreview
);
if (IsPreview) {
MainButton.disabled = true;
return Tile;
}
if (
SelectedGardenTool ===
"Trowel"
) {
MainButton.addEventListener(
"click",
() => {
PlantSeed(
PlotIndex
);
}
);
} else if (
SelectedGardenTool ===
"MagicTrowel"
) {
Tile.classList.add(
"GardenPlotMagicPlant"
);
MainButton.title =
"Fill empty plots with the selected seed";
MainButton.addEventListener(
"click",
PlantManySeeds
);
} else {
MainButton.disabled = true;
}
return Tile;
}
if (Plant === undefined) {
Visual.textContent = "?";
AppendGardenPlotFooter(
Tile,
Plot,
null,
0,
PlotIndex,
DisplaySettings,
IsPreview
);
MainButton.disabled = true;
return Tile;
}
const Progress =
GetPlantGrowthProgress(
Plot,
Plant
);
Tile.style.setProperty(
"--GrowthProgress",
`${Progress * 100}%`
);
const ImagePath =
GetPlantImage(
Plot,
Plant
);
if (ImagePath !== null) {
const Image =
document.createElement(
"img"
);
Image.className =
"PlantSprite";
Image.alt =
Plant.Name;
Image.src =
ImagePath;
Visual.appendChild(
Image
);
} else {
const MissingImage =
document.createElement(
"span"
);
MissingImage.className =
"GardenPlantMissing";
MissingImage.textContent =
Plant.Name;
Visual.appendChild(
MissingImage
);
}
AppendGardenPlotFooter(
Tile,
Plot,
Plant,
Progress,
PlotIndex,
DisplaySettings,
IsPreview
);
if (IsPreview) {
MainButton.title =
"Future Sight: " +
Plant.Name;
MainButton.disabled = true;
return Tile;
}
if (
SelectedGardenTool ===
"Shovel"
) {
Tile.classList.add(
"GardenPlotRemove"
);
MainButton.title =
"Remove " +
Plant.Name;
MainButton.addEventListener(
"click",
() => {
RemovePlant(
PlotIndex
);
}
);
return Tile;
}
if (
SelectedGardenTool ===
"Fertilizer"
) {
if (Progress >= 1) {
MainButton.title =
Plant.Name +
" is already fully grown";
MainButton.disabled = true;
return Tile;
}
Tile.classList.add(
"GardenPlotFertilize"
);
MainButton.title =
"Fertilize " +
Plant.Name +
" (" +
GetFertilizerGrowthMinutes(
GameSave
) +
" minutes)";
MainButton.addEventListener(
"click",
() => {
FertilizePlant(
PlotIndex
);
}
);
return Tile;
}
if (
SelectedGardenTool !==
"Trowel"
) {
MainButton.title =
Plant.Name +
" - " +
Math.floor(
Progress * 100
) +
"% grown";
MainButton.disabled = true;
return Tile;
}
if (
Progress >= 1
) {
Tile.classList.add(
"GardenPlotMature"
);
MainButton.title =
"Harvest " +
Plant.Name;
MainButton.addEventListener(
"click",
() => {
HarvestPlant(
PlotIndex
);
}
);
} else {
MainButton.title =
Plant.Name +
" - " +
Math.floor(
Progress * 100
) +
"% grown";
MainButton.disabled = true;
}
return Tile;
}
function AppendGardenPlotFooter(
Tile,
Plot,
Plant,
Progress,
PlotIndex,
DisplaySettings,
IsPreview
) {
const ShowTimer =
DisplaySettings.ShowGrowthTimers;
const ShowRotation =
Plot !== null &&
DisplaySettings.ShowPlotRotation &&
!IsPreview;
if (
!ShowTimer &&
!ShowRotation
) {
return;
}
Tile.classList.add(
"GardenPlotWithFooter"
);
const Footer =
document.createElement(
"span"
);
Footer.className =
"GardenPlotFooter";
if (
ShowTimer &&
ShowRotation
) {
Footer.classList.add(
"GardenPlotFooterSplit"
);
}
if (ShowTimer) {
Footer.appendChild(
CreateGardenPlotTimer(
Plot,
Plant,
Progress
)
);
}
if (ShowRotation) {
const RotateButton =
document.createElement(
"button"
);
RotateButton.className =
"GardenPlotRotationButton";
RotateButton.type = "button";
RotateButton.textContent =
GetGardenPlotRotationLabel(
Plot
);
RotateButton.title =
"Rotate plot clockwise";
RotateButton.setAttribute(
"aria-label",
"Rotate " +
(Plant?.Name ?? "plot") +
" clockwise"
);
RotateButton.addEventListener(
"click",
() => {
RotateGardenPlot(
PlotIndex
);
}
);
Footer.appendChild(
RotateButton
);
}
Tile.appendChild(Footer);
}
function CreateGardenPlotName(
Plot,
Plant
) {
const Name =
document.createElement(
"span"
);
Name.className =
"GardenPlotName";
if (Plot === null) {
Name.textContent =
"Empty";
} else if (Plant === undefined) {
Name.textContent =
"Unknown";
} else {
Name.textContent =
Plant.Name;
Name.title =
Plant.Name;
}
return Name;
}
function CreateGardenPlotTimer(
Plot,
Plant,
Progress
) {
const Timer =
document.createElement(
"span"
);
Timer.className =
"GardenPlotTimer";
if (
Plot === null ||
Plant === null ||
Plant === undefined
) {
Timer.textContent = "";
return Timer;
}
if (Progress >= 1) {
Timer.textContent =
"Ready";
return Timer;
}
const RemainingTime =
Math.max(
0,
Plant.GrowthTime -
(
GetSimulationTime(
GameSave
) -
Plot.PlantedAt
)
);
Timer.textContent =
FormatGardenRemainingTime(
RemainingTime
);
return Timer;
}
function FormatGardenRemainingTime(
Milliseconds
) {
const TotalSeconds =
Math.max(
0,
Math.ceil(
Number(
Milliseconds
) / 1000
)
);
const Hours =
Math.floor(
TotalSeconds / 3600
);
const Minutes =
Math.floor(
(
TotalSeconds % 3600
) / 60
);
const Seconds =
TotalSeconds % 60;
if (Hours > 0) {
return (
Hours +
":" +
String(Minutes).padStart(
2,
"0"
) +
":" +
String(Seconds).padStart(
2,
"0"
)
);
}
return (
Minutes +
":" +
String(Seconds).padStart(
2,
"0"
)
);
}
async function PlantSeed(
PlotIndex
) {
const ActionTime = Date.now();
AdvanceGameSimulation(
GameSave,
ActionTime
);
if (
GameSave.Garden.Plots[
PlotIndex
] !== null
) {
return;
}
EnsureSelectedSeed();
if (SelectedSeedId === null) {
SetGardenMessage(
"You don't have any seeds to plant."
);
return;
}
const Plant =
GetPlantById(
SelectedSeedId
);
const PlantKey =
GetPlantKeyById(
SelectedSeedId
);
if (
Plant === null ||
PlantKey === null
) {
return;
}
if (
!TakeSeed(
GameSave,
SelectedSeedId
)
) {
EnsureSelectedSeed();
RenderSeeds();
return;
}
GameSave.Garden.Plots[
PlotIndex
] = {
Plant: PlantKey,
PlantedAt: ActionTime,
Rotation: GardenPlotDefaultRotation
};
SetGardenMessage(
"Planted " +
Plant.Name +
"."
);
CheckGardenMutations(
GameSave,
ActionTime
);
RenderGame();
await SaveGame(
GameSave
);
}
async function PlantManySeeds() {
const ActionTime = Date.now();
AdvanceGameSimulation(
GameSave,
ActionTime
);
EnsureSelectedSeed();
if (SelectedSeedId === null) {
SetGardenMessage(
"You don't have any seeds to plant."
);
return;
}
const Plant =
GetPlantById(
SelectedSeedId
);
const PlantKey =
GetPlantKeyById(
SelectedSeedId
);
if (
Plant === null ||
PlantKey === null
) {
return;
}
const EmptyPlotIndexes = [];
GameSave.Garden.Plots.forEach(
(Plot, PlotIndex) => {
if (Plot === null) {
EmptyPlotIndexes.push(
PlotIndex
);
}
}
);
if (EmptyPlotIndexes.length === 0) {
SetGardenMessage(
"There aren't any empty plots to fill."
);
return;
}
const SeedCount =
GetSeedCount(
GameSave,
SelectedSeedId
);
const PlantCount = Math.min(
SeedCount,
EmptyPlotIndexes.length
);
if (PlantCount <= 0) {
EnsureSelectedSeed();
RenderGame();
SetGardenMessage(
"You don't have any of that seed left."
);
return;
}
if (
!TakeSeed(
GameSave,
SelectedSeedId,
PlantCount
)
) {
return;
}
const PlantedAt = ActionTime;
for (
let Index = 0;
Index < PlantCount;
Index++
) {
GameSave.Garden.Plots[
EmptyPlotIndexes[Index]
] = {
Plant: PlantKey,
PlantedAt: PlantedAt,
Rotation: GardenPlotDefaultRotation
};
}
SetGardenMessage(
"Magic trowel planted " +
PlantCount.toLocaleString() +
" " +
Plant.Name +
(PlantCount === 1
? " seed."
: " seeds.")
);
CheckGardenMutations(
GameSave,
ActionTime
);
RenderGame();
await SaveGame(
GameSave
);
}
async function HarvestPlant(
PlotIndex
) {
const ActionTime = Date.now();
AdvanceGameSimulation(
GameSave,
ActionTime
);
const Plot =
GameSave.Garden.Plots[
PlotIndex
];
if (Plot === null) {
return;
}
const Plant =
Plants[Plot.Plant];
if (
Plant === undefined ||
!IsPlantMature(
Plot,
Plant
)
) {
return;
}
const RewardAmount =
GetPlantHarvestReward(
GameSave,
Plant.Id
);
if (RewardAmount === null) {
SetGardenMessage(
"Couldn't calculate the harvest reward for " +
Plant.Name +
"."
);
return;
}
GiveReward({
Currency: "Dew",
Amount: RewardAmount
});
GameSave.Garden.Plots[
PlotIndex
] = null;
SetGardenMessage(
"Harvested " +
Plant.Name +
" for " +
RewardAmount +
" Dew."
);
CheckGardenMutations(
GameSave,
ActionTime
);
RenderGame();
await SaveGame(
GameSave
);
}
async function RemovePlant(
PlotIndex
) {
const ActionTime = Date.now();
AdvanceGameSimulation(
GameSave,
ActionTime
);
const Plot =
GameSave.Garden.Plots[
PlotIndex
];
if (Plot === null) {
return;
}
const Plant =
Plants[Plot.Plant];
let ReturnMessage = "";
if (
Plant !== undefined &&
DoesShovelReturnSeed(
GameSave
)
) {
AddSeed(
GameSave,
Plant.Id,
1
);
ReturnMessage =
" The seed was returned.";
} else if (
Plant !== undefined
) {
const RefundRate =
GetShovelRefundRate(
GameSave
);
const SeedCost =
GetPlantShopCost(
GameSave,
Plant.Id
);
const Refund =
SeedCost === null
? 0
: Math.floor(
SeedCost *
RefundRate
);
if (Refund > 0) {
GameSave.Currency.Dew +=
Refund;
ReturnMessage =
" Recovered " +
Refund.toLocaleString() +
" Dew.";
}
}
GameSave.Garden.Plots[
PlotIndex
] = null;
SetGardenMessage(
"Removed " +
(Plant?.Name ?? "plant") +
"." +
ReturnMessage
);
CheckGardenMutations(
GameSave,
ActionTime
);
RenderGame();
await SaveGame(
GameSave
);
}
async function FertilizePlant(
PlotIndex
) {
const ActionTime = Date.now();
AdvanceGameSimulation(
GameSave,
ActionTime
);
const Plot =
GameSave.Garden.Plots[
PlotIndex
];
if (Plot === null) {
return;
}
const Plant =
Plants[Plot.Plant];
if (
Plant === undefined ||
IsPlantMature(
Plot,
Plant
)
) {
return;
}
if (
!TakeFertilizerUse(
GameSave
)
) {
SetGardenMessage(
"You're out of Fertilizer. Buy more in the Shop."
);
RenderTools();
return;
}
const Minutes =
GetFertilizerGrowthMinutes(
GameSave
);
Plot.PlantedAt -=
Minutes * 60 * 1000;
SetGardenMessage(
"Fertilized " +
Plant.Name +
", advancing its growth by " +
Minutes +
(Minutes === 1
? " minute."
: " minutes.")
);
CheckGardenMutations(
GameSave,
ActionTime
);
RenderGame();
await SaveGame(
GameSave
);
}
async function UseFutureSight() {
const ActionTime = Date.now();
AdvanceGameSimulation(
GameSave,
ActionTime
);
if (
!IsGardenToolUnlocked(
GameSave,
"FutureSight"
) ||
!IsFutureSightReady(
GameSave
)
) {
return;
}
const Preview =
CreateFutureSightPreview();
if (Preview === null) {
SelectedGardenTool =
"Trowel";
SetGardenMessage(
"Future Sight can't see a mutation from the current Garden."
);
RenderTools();
RenderGarden();
return;
}
FutureSightPreview = Preview;
StartFutureSightCooldown(
GameSave
);
SetGardenMessage(
"Future Sight: highlighted plots show the next mutation succeeding."
);
RenderSeeds();
RenderTools();
RenderGarden();
await SaveGame(
GameSave
);
}
function CreateFutureSightPreview() {
const PreviewSave =
NormalizeSaveData(
JSON.parse(
JSON.stringify(
GameSave
)
)
);
const Candidates =
FindGardenMutationCandidates(
PreviewSave
)
.filter(
Candidate =>
!IsMutationCandidateOnCooldown(
PreviewSave,
Candidate
)
)
.sort(
CompareMutationCandidates
);
if (Candidates.length === 0) {
return null;
}
const Candidate =
Candidates[0];
ApplyMutationResult(
PreviewSave,
Candidate,
Candidate.Success
);
return {
MutationId:
Candidate.Mutation.Id,
Plots:
PreviewSave.Garden.Plots.map(
Plot =>
CloneMutationPlot(
Plot
)
)
};
}
async function GardenTick() {
if (MutationCheckPending) {
return;
}
MutationCheckPending = true;
try {
const SimulationResult =
AdvanceGameSimulation(
GameSave,
Date.now()
);
if (
SimulationResult.Attempted &&
FutureSightPreview !== null
) {
FutureSightPreview = null;
if (
SelectedGardenTool ===
"FutureSight"
) {
SelectedGardenTool =
"Trowel";
}
}
RenderGame();
if (SimulationResult.Dirty) {
await SaveGame(
GameSave
);
}
} finally {
MutationCheckPending = false;
}
}
function GiveReward(
Reward
) {
GameSave.Currency[
Reward.Currency
] ??= 0;
GameSave.Currency[
Reward.Currency
] += Reward.Amount;
GameSave.Statistics ??= {};
GameSave.Statistics.CurrencyEarned ??= {};
GameSave.Statistics.CurrencyEarned[
Reward.Currency
] ??= 0;
GameSave.Statistics.CurrencyEarned[
Reward.Currency
] += Reward.Amount;
}
function GetPlantGrowthProgress(
Plot,
Plant
) {
if (
Plant.GrowthTime <= 0
) {
return 1;
}
const Age =
GetSimulationTime(
GameSave
) -
Plot.PlantedAt;
return Math.max(
0,
Math.min(
Age /
Plant.GrowthTime,
1
)
);
}
function GetPlantTags(
Plot,
Plant
) {
return [
...(Plant.Tags ?? [])
];
}
function IsPlantMature(
Plot,
Plant
) {
return (
GetPlantGrowthProgress(
Plot,
Plant
) >= 1
);
}
function GetPlantImages(
Plot,
Plant
) {
const Direction =
Plot !== null &&
Plant?.DirectionalSprites === true
? GetGardenPlotRotation(
Plot
)
: null;
return GetPlantImageSources(
Plant ?? Plot.Plant,
Direction
);
}
function GetPlantImage(
Plot,
Plant
) {
const Images =
GetPlantImages(
Plot,
Plant
);
if (Images.length === 0) {
return null;
}
if (Images.length === 1) {
return Images[0];
}
const Progress =
GetPlantGrowthProgress(
Plot,
Plant
);
if (Progress >= 1) {
return Images[
Images.length - 1
];
}
const GrowingImageCount =
Images.length - 1;
const ImageIndex =
Math.min(
Math.floor(
Progress *
GrowingImageCount
),
GrowingImageCount - 1
);
return Images[
ImageIndex
];
}
function SetGardenMessage(
Message
) {
document.getElementById(
"GardenMessage"
).textContent = Message;
}
function GetNextHarvestInfo() {
let NextHarvest = null;
for (
const Plot
of GameSave.Garden.Plots
) {
if (Plot === null) {
continue;
}
const Plant =
Plants[Plot.Plant];
if (Plant === undefined) {
continue;
}
const HarvestTime =
Plot.PlantedAt +
Plant.GrowthTime;
if (
NextHarvest === null ||
HarvestTime <
NextHarvest.Time
) {
NextHarvest = {
Time: HarvestTime,
Plant
};
}
}
return NextHarvest;
}
function RenderNextHarvest() {
const Line =
document.getElementById(
"NextHarvestLine"
);
const Element =
document.getElementById(
"NextHarvest"
);
if (
Line === null ||
Element === null
) {
return;
}
const HasOverview =
HasGardenOverviewUpgrade(
GameSave
);
const ShowNextHarvest =
!HasOverview ||
GameSave.Preferences
.ShowNextHarvest !== false;
Line.hidden = !ShowNextHarvest;
if (!ShowNextHarvest) {
return;
}
const Harvest =
GetNextHarvestInfo();
if (Harvest === null) {
Element.textContent =
"Nothing planted";
Element.removeAttribute(
"title"
);
return;
}
const Remaining =
Harvest.Time -
GetSimulationTime(
GameSave
);
const IncludePlantName =
HasOverview;
if (Remaining <= 0) {
Element.textContent =
IncludePlantName
? Harvest.Plant.Name +
" is ready!"
: "Ready now!";
Element.removeAttribute(
"title"
);
return;
}
const RemainingText =
FormatRemainingTime(
Remaining
);
Element.textContent =
IncludePlantName
? Harvest.Plant.Name +
" in " +
RemainingText
: RemainingText;
const HarvestDate =
new Date(
Harvest.Time
);
Element.title =
"Ready at " +
HarvestDate.toLocaleTimeString(
[],
{
hour: "2-digit",
minute: "2-digit"
}
);
}
function RenderGardenOverview() {
const Overview =
document.getElementById(
"GardenOverviewDetails"
);
if (Overview === null) {
return;
}
if (
!HasGardenOverviewUpgrade(
GameSave
)
) {
Overview.hidden = true;
return;
}
const TotalPlots =
GameSave.Garden.Width *
GameSave.Garden.Height;
let PlantedPlots = 0;
let GrowingPlots = 0;
let ReadyPlots = 0;
for (
const Plot
of GameSave.Garden.Plots
) {
if (Plot === null) {
continue;
}
PlantedPlots++;
const Plant =
Plants[Plot.Plant];
if (Plant === undefined) {
continue;
}
if (
IsPlantMature(
Plot,
Plant
)
) {
ReadyPlots++;
} else {
GrowingPlots++;
}
}
const EmptyPlots =
Math.max(
0,
TotalPlots -
PlantedPlots
);
const Lines = [
{
LineId:
"GardenSizeOverviewLine",
ValueId:
"GardenSizeOverview",
Preference:
"ShowGardenSize",
Value:
GameSave.Garden.Width +
"×" +
GameSave.Garden.Height +
" (" +
TotalPlots.toLocaleString() +
")"
},
{
LineId:
"EmptyPlotsOverviewLine",
ValueId:
"EmptyPlotsOverview",
Preference:
"ShowEmptyPlots",
Value:
EmptyPlots.toLocaleString() +
"/" +
TotalPlots.toLocaleString()
},
{
LineId:
"PlantedPlotsOverviewLine",
ValueId:
"PlantedPlotsOverview",
Preference:
"ShowPlantedPlots",
Value:
PlantedPlots.toLocaleString() +
"/" +
TotalPlots.toLocaleString()
},
{
LineId:
"GrowingPlotsOverviewLine",
ValueId:
"GrowingPlotsOverview",
Preference:
"ShowGrowingPlots",
Value:
GrowingPlots.toLocaleString() +
"/" +
TotalPlots.toLocaleString()
},
{
LineId:
"ReadyPlotsOverviewLine",
ValueId:
"ReadyPlotsOverview",
Preference:
"ShowReadyPlots",
Value:
ReadyPlots.toLocaleString() +
"/" +
TotalPlots.toLocaleString()
}
];
let VisibleLines = 0;
for (
const Line
of Lines
) {
const LineElement =
document.getElementById(
Line.LineId
);
const ValueElement =
document.getElementById(
Line.ValueId
);
if (
LineElement === null ||
ValueElement === null
) {
continue;
}
const IsVisible =
GameSave.Preferences[
Line.Preference
] !== false;
LineElement.hidden =
!IsVisible;
ValueElement.textContent =
Line.Value;
if (IsVisible) {
VisibleLines++;
}
}
Overview.hidden =
VisibleLines === 0;
}
function FormatRemainingTime(
Milliseconds
) {
const TotalSeconds =
Math.ceil(
Milliseconds /
1000
);
const Days =
Math.floor(
TotalSeconds /
86400
);
const Hours =
Math.floor(
(
TotalSeconds %
86400
) /
3600
);
const Minutes =
Math.floor(
(
TotalSeconds %
3600
) /
60
);
const Seconds =
TotalSeconds %
60;
if (Days > 0) {
return (
Days +
"d " +
Hours +
"h"
);
}
if (Hours > 0) {
return (
Hours +
"h " +
Minutes +
"m"
);
}
if (Minutes > 0) {
return (
Minutes +
"m " +
Seconds +
"s"
);
}
return (
Seconds +
"s"
);
}
function RenderGardenEconomy() {
const Details =
document.getElementById(
"GardenEconomyDetails"
);
if (Details === null) {
return;
}
if (
!HasGardenEconomyUpgrade(
GameSave
)
) {
Details.hidden = true;
return;
}
Details.hidden = false;
const Summary =
GetGardenEconomySummary(
GameSave
);
const Values = {
GardenDewInvested:
Summary === null
? "Unavailable"
: Summary.DewInvested
.toLocaleString(),
GardenHarvestValue:
Summary === null
? "Unavailable"
: Summary.HarvestValue
.toLocaleString(),
GardenNetProfit:
Summary === null
? "Unavailable"
: Summary.NetProfit
.toLocaleString(),
GardenDewPerHour:
Summary === null
? "Unavailable"
: FormatDewPerHour(
Summary.DewPerHour
)
};
for (
const [ElementId, Value]
of Object.entries(Values)
) {
const Element =
document.getElementById(
ElementId
);
if (Element !== null) {
Element.textContent =
Value;
}
}
}
function RenderGardenInfoLayout() {
const DetailRow =
document.getElementById(
"GardenInfoDetailRow"
);
const OverviewSection =
document.getElementById(
"GardenOverviewSection"
);
const EconomySection =
document.getElementById(
"GardenEconomySection"
);
if (
DetailRow === null ||
OverviewSection === null ||
EconomySection === null
) {
return;
}
const HasOverview =
HasGardenOverviewUpgrade(
GameSave
);
const HasEconomy =
HasGardenEconomyUpgrade(
GameSave
);
DetailRow.hidden =
!HasOverview &&
!HasEconomy;
OverviewSection.classList.toggle(
"GardenInfoSectionEmpty",
!HasOverview
);
EconomySection.classList.toggle(
"GardenInfoSectionEmpty",
!HasEconomy
);
}
document.addEventListener(
"DOMContentLoaded",
StartGame
);