Added rotation to plots.. Only does stuff for functional plants.

This commit is contained in:
sarah committed 2026-09-05 16:32:41 +02:00
1 parent 75e4f31ccd
commit 132aba0b9a
14 files changed
+1244 -197

No files matched your search

+70 -9
View File
@@ -246,6 +246,17 @@ $PageSection = "Admin";
</span>
</label>
<label class="AdminEditorCheckField">
<input
id="AdminPlantDirectionalSprites"
name="DirectionalSprites"
type="checkbox"
>
<span>
Directional sprites
</span>
</label>
<label class="AdminEditorField AdminEditorWideField">
<span>Effects JSON</span>
<textarea
@@ -323,25 +334,75 @@ $PageSection = "Admin";
Optional. You can save a plant with no sprites and add them later.
Upload PNG files named by stage, such as
<code>1.png</code>, <code>2.png</code>, <code>3.png</code>.
Directional plants use one stage set for each direction.
</p>
<div class="AdminPlantImageUpload">
<div
id="AdminPlantDefaultImageUpload"
class="AdminPlantImageUpload"
>
<input
id="AdminPlantImageFiles"
type="file"
accept="image/png,.png"
multiple
>
<button
id="AdminPlantImageUploadButton"
class="ActionButton AdminInlineButton"
type="button"
>
Upload selected sprites
</button>
</div>
<div
id="AdminPlantDirectionalImageUpload"
class="AdminPlantDirectionalImageUpload"
hidden
>
<label class="AdminEditorField">
<span>North sprites</span>
<input
id="AdminPlantImageFilesNorth"
type="file"
accept="image/png,.png"
multiple
>
</label>
<label class="AdminEditorField">
<span>East sprites</span>
<input
id="AdminPlantImageFilesEast"
type="file"
accept="image/png,.png"
multiple
>
</label>
<label class="AdminEditorField">
<span>South sprites</span>
<input
id="AdminPlantImageFilesSouth"
type="file"
accept="image/png,.png"
multiple
>
</label>
<label class="AdminEditorField">
<span>West sprites</span>
<input
id="AdminPlantImageFilesWest"
type="file"
accept="image/png,.png"
multiple
>
</label>
</div>
<button
id="AdminPlantImageUploadButton"
class="ActionButton AdminInlineButton"
type="button"
>
Upload selected sprites
</button>
<p
id="AdminPlantImageNote"
class="AdminPlantImageNote"
+24
View File
@@ -159,6 +159,30 @@ $PageTitle = "Profile";
to use these settings.
</p>
<h3>Plot rotation</h3>
<p>
Choose whether plot directions are shown
beside plot timers.
</p>
<div class="ProfileButtonRow">
<button
id="TogglePlotRotationButton"
type="button"
>
Plot rotation: Locked
</button>
</div>
<p
id="PlotRotationDisplayMessage"
class="ProfileMessage"
>
Unlock Plot rotation in the Shop
to use this setting.
</p>
<h3>Garden overview</h3>
<p>
+2
View File
@@ -402,6 +402,8 @@ function BuildAdminMassPlantPayload(
Source.Effects ?? {}
)
),
DirectionalSprites:
Source.DirectionalSprites === true,
ShopPlant:
Source.Shop?.ShopPlant === true,
BaseCost:
+59 -2
View File
@@ -95,10 +95,23 @@ function GetAdminOverviewMissingSprites(
)
);
const DirectionalStages =
Array.isArray(
Plant.DirectionalImageStages
)
? Plant.DirectionalImageStages
: [];
const HighestApiStage =
Math.max(
0,
...ApiStages
...ApiStages,
...DirectionalStages.map(
Stage =>
Number(
Stage?.Stage ?? 0
)
)
);
const HighestStage =
@@ -110,6 +123,51 @@ function GetAdminOverviewMissingSprites(
const Missing = [];
if (Plant.DirectionalSprites === true) {
const Directions = [
["N", "North"],
["E", "East"],
["S", "South"],
["W", "West"]
];
for (const [Code, Name] of Directions) {
const Stages = new Set(
DirectionalStages
.filter(
Stage =>
String(
Stage?.Direction ?? ""
).toUpperCase() === Code
)
.map(
Stage =>
Number(Stage.Stage)
)
.filter(
Stage =>
Number.isInteger(Stage) &&
Stage > 0
)
);
for (
let Stage = 1;
Stage <= HighestStage;
Stage++
) {
if (!Stages.has(Stage)) {
Missing.push(
Name + "/" +
Stage + ".png"
);
}
}
}
return Missing;
}
for (
let Stage = 1;
Stage <= HighestStage;
@@ -132,7 +190,6 @@ function GetAdminOverviewMissingSprites(
return Missing;
}
function CollectAdminOverviewCreatedPlants() {
const Created = new Set();
+324 -88
View File
@@ -168,6 +168,16 @@ function BindAdminPlantEditor() {
}
);
document.getElementById(
"AdminPlantDirectionalSprites"
).addEventListener(
"change",
() => {
RenderAdminPlantImageManager();
RenderAdminPlantPreview();
}
);
document.getElementById(
"AdminPlantImageUploadButton"
).addEventListener(
@@ -540,6 +550,11 @@ function LoadAdminPlantIntoForm(
).checked =
Plant.Shop?.ShopPlant === true;
document.getElementById(
"AdminPlantDirectionalSprites"
).checked =
Plant.DirectionalSprites === true;
SetAdminPlantField(
"AdminPlantBaseCost",
Plant.Shop?.BaseCost ?? ""
@@ -648,6 +663,11 @@ function DuplicateAdminPlant() {
).checked =
SourcePlant.Shop?.ShopPlant === true;
document.getElementById(
"AdminPlantDirectionalSprites"
).checked =
SourcePlant.DirectionalSprites === true;
SetAdminPlantField(
"AdminPlantBaseCost",
SourcePlant.Shop?.BaseCost ?? ""
@@ -734,6 +754,10 @@ function StartNewAdminPlant() {
"AdminPlantShopPlant"
).checked = false;
document.getElementById(
"AdminPlantDirectionalSprites"
).checked = false;
SetAdminPlantField(
"AdminPlantBaseCost",
""
@@ -910,6 +934,11 @@ function GetAdminPlantFormData() {
Effects:
ParseAdminPlantEffects(),
DirectionalSprites:
document.getElementById(
"AdminPlantDirectionalSprites"
).checked,
ShopPlant: ShopPlant,
BaseCost:
@@ -1249,6 +1278,10 @@ function GetAdminPlantPreviewDefinition() {
"AdminPlantHarvestMultiplier"
).value
),
DirectionalSprites:
document.getElementById(
"AdminPlantDirectionalSprites"
).checked,
Shop: {
ShopPlant: ShopPlant,
BaseCost:
@@ -1426,16 +1459,43 @@ function RenderAdminPlantImageManager() {
"AdminPlantImageUploadButton"
);
const FileInput =
const DefaultUpload =
document.getElementById(
"AdminPlantImageFiles"
"AdminPlantDefaultImageUpload"
);
const DirectionalUpload =
document.getElementById(
"AdminPlantDirectionalImageUpload"
);
const DirectionalToggle =
document.getElementById(
"AdminPlantDirectionalSprites"
);
const Inputs = [
"AdminPlantImageFiles",
"AdminPlantImageFilesNorth",
"AdminPlantImageFilesEast",
"AdminPlantImageFilesSouth",
"AdminPlantImageFilesWest"
]
.map(
ElementId =>
document.getElementById(
ElementId
)
)
.filter(Boolean);
if (
List === null ||
Note === null ||
UploadButton === null ||
FileInput === null
DefaultUpload === null ||
DirectionalUpload === null ||
DirectionalToggle === null
) {
return;
}
@@ -1449,6 +1509,15 @@ function RenderAdminPlantImageManager() {
AdminPlantEditingKey
] ?? null;
const IsDirectional =
DirectionalToggle.checked;
DefaultUpload.hidden =
IsDirectional;
DirectionalUpload.hidden =
!IsDirectional;
const CanEdit =
Plant !== null &&
!AdminPlantImagePending;
@@ -1456,8 +1525,9 @@ function RenderAdminPlantImageManager() {
UploadButton.disabled =
!CanEdit;
FileInput.disabled =
!CanEdit;
for (const Input of Inputs) {
Input.disabled = !CanEdit;
}
if (Plant === null) {
Note.textContent =
@@ -1466,6 +1536,7 @@ function RenderAdminPlantImageManager() {
}
const StaticImages =
typeof PlantImages !== "undefined" &&
Array.isArray(
PlantImages[
AdminPlantEditingKey
@@ -1488,6 +1559,44 @@ function RenderAdminPlantImageManager() {
)
: [];
const DirectionOrder = {
N: 0,
E: 1,
S: 2,
W: 3
};
const DirectionNames = {
N: "North",
E: "East",
S: "South",
W: "West"
};
const DirectionalStages =
Array.isArray(
Plant.DirectionalImageStages
)
? [...Plant.DirectionalImageStages]
.sort(
(A, B) =>
Number(A.Stage) -
Number(B.Stage) ||
(
DirectionOrder[
String(A.Direction)
.toUpperCase()
] ?? 99
) -
(
DirectionOrder[
String(B.Direction)
.toUpperCase()
] ?? 99
)
)
: [];
const FallbackText =
StaticImages.length > 0
? " Repository fallback: " +
@@ -1500,28 +1609,45 @@ function RenderAdminPlantImageManager() {
)
: "";
if (ApiStages.length === 0) {
const DisplayStages =
IsDirectional
? DirectionalStages
: ApiStages;
if (DisplayStages.length === 0) {
Note.textContent =
"No API sprites uploaded." +
FallbackText;
IsDirectional
? "No directional API sprites uploaded. Choose stage-numbered PNGs for each direction." +
FallbackText
: "No API sprites uploaded." +
FallbackText;
return;
}
Note.textContent =
ApiStages.length +
" API sprite" +
DisplayStages.length +
(IsDirectional
? " directional API sprite"
: " API sprite") +
(
ApiStages.length === 1
DisplayStages.length === 1
? " uploaded."
: "s uploaded."
) +
FallbackText;
for (const ImageStage of ApiStages) {
for (const ImageStage of DisplayStages) {
const Stage = Number(
ImageStage.Stage
);
const Direction =
IsDirectional
? String(
ImageStage.Direction ?? ""
).toUpperCase()
: null;
const Row =
document.createElement(
"div"
@@ -1550,13 +1676,24 @@ function RenderAdminPlantImageManager() {
GetAdminPlantApiImageUrl(
Plant.Id,
Stage,
ImageStage.Revision
ImageStage.Revision,
Direction
);
Image.alt =
Plant.Name +
" stage " +
Stage;
Stage +
(
Direction === null
? ""
: " " +
(
DirectionNames[
Direction
] ?? Direction
)
);
Preview.appendChild(Image);
@@ -1566,7 +1703,17 @@ function RenderAdminPlantImageManager() {
);
Label.textContent =
"Stage " + Stage;
"Stage " + Stage +
(
Direction === null
? ""
: " · " +
(
DirectionNames[
Direction
] ?? Direction
)
);
const RemoveButton =
document.createElement(
@@ -1586,7 +1733,8 @@ function RenderAdminPlantImageManager() {
() =>
DeleteAdminPlantImage(
Plant.Id,
Stage
Stage,
Direction
)
);
@@ -1604,14 +1752,24 @@ function RenderAdminPlantImageManager() {
function GetAdminPlantApiImageUrl(
PlantId,
Stage,
Revision = 0
Revision = 0,
Direction = null
) {
const QueryData = {
PlantId: String(PlantId),
Stage: String(Stage),
v: String(Revision ?? 0)
};
if (Direction !== null) {
QueryData.Direction =
String(Direction);
}
const Query =
new URLSearchParams({
PlantId: String(PlantId),
Stage: String(Stage),
v: String(Revision ?? 0)
});
new URLSearchParams(
QueryData
);
return ApiUrl +
"/PlantImage.php?" +
@@ -1619,6 +1777,41 @@ function GetAdminPlantApiImageUrl(
}
function GetAdminPlantImageUploadSets() {
const IsDirectional =
document.getElementById(
"AdminPlantDirectionalSprites"
).checked;
if (!IsDirectional) {
return [
{
Direction: null,
Input:
document.getElementById(
"AdminPlantImageFiles"
)
}
];
}
return [
["N", "AdminPlantImageFilesNorth"],
["E", "AdminPlantImageFilesEast"],
["S", "AdminPlantImageFilesSouth"],
["W", "AdminPlantImageFilesWest"]
].map(
([Direction, ElementId]) => ({
Direction: Direction,
Input:
document.getElementById(
ElementId
)
})
);
}
async function UploadAdminPlantImages() {
if (
AdminPlantImagePending ||
@@ -1636,67 +1829,61 @@ async function UploadAdminPlantImages() {
return;
}
const FileInput =
document.getElementById(
"AdminPlantImageFiles"
);
const Files =
Array.from(
FileInput.files ?? []
);
if (Files.length === 0) {
SetAdminPlantMessage(
"Choose one or more PNG sprites first."
);
return;
}
const Stages = new Set();
const Uploads = [];
try {
for (const File of Files) {
const Match =
/^(\\d+)\\.png$/i.exec(
File.name
);
if (Match === null) {
throw new Error(
"Sprite filenames must be stage numbers such as 1.png or 3.png."
);
}
const Stage = Number(
Match[1]
for (
const UploadSet
of GetAdminPlantImageUploadSets()
) {
const Files = Array.from(
UploadSet.Input?.files ?? []
);
if (
!Number.isInteger(Stage) ||
Stage < 1 ||
Stage > 255
) {
throw new Error(
"Sprite stages must be between 1 and 255."
const Stages = new Set();
for (const File of Files) {
const Match =
/^(\d+)\.png$/i.exec(
File.name
);
if (Match === null) {
throw new Error(
"Sprite filenames must be stage numbers such as 1.png or 3.png."
);
}
const Stage = Number(
Match[1]
);
if (
!Number.isInteger(Stage) ||
Stage < 1 ||
Stage > 255
) {
throw new Error(
"Sprite stages must be between 1 and 255."
);
}
if (Stages.has(Stage)) {
throw new Error(
"Stage " + Stage +
" was selected more than once for the same direction."
);
}
Stages.add(Stage);
Uploads.push({
Stage: Stage,
Direction:
UploadSet.Direction,
File: File
});
}
if (Stages.has(Stage)) {
throw new Error(
"Stage " +
Stage +
" was selected more than once."
);
}
Stages.add(Stage);
Uploads.push({
Stage: Stage,
File: File
});
}
} catch (Error) {
SetAdminPlantMessage(
@@ -1705,6 +1892,13 @@ async function UploadAdminPlantImages() {
return;
}
if (Uploads.length === 0) {
SetAdminPlantMessage(
"Choose one or more PNG sprites first."
);
return;
}
AdminPlantImagePending = true;
RenderAdminPlantImageManager();
@@ -1719,18 +1913,32 @@ async function UploadAdminPlantImages() {
Upload.File
);
const RequestData = {
PlantId: Plant.Id,
Stage: Upload.Stage,
MimeType: "image/png",
DataBase64: DataBase64
};
if (Upload.Direction !== null) {
RequestData.Direction =
Upload.Direction;
}
await AdminPlantRequest(
"UploadImage",
{
PlantId: Plant.Id,
Stage: Upload.Stage,
MimeType: "image/png",
DataBase64: DataBase64
}
RequestData
);
}
FileInput.value = "";
for (
const UploadSet
of GetAdminPlantImageUploadSets()
) {
if (UploadSet.Input !== null) {
UploadSet.Input.value = "";
}
}
localStorage.removeItem(
"SarahtoninGardenContent"
@@ -1818,7 +2026,8 @@ function ReadAdminPlantFileBase64(
async function DeleteAdminPlantImage(
PlantId,
Stage
Stage,
Direction = null
) {
if (AdminPlantImagePending) {
return;
@@ -1830,16 +2039,28 @@ async function DeleteAdminPlantImage(
SetAdminPlantMessage(
"Removing stage " +
Stage +
(
Direction === null
? ""
: " " + Direction
) +
" sprite..."
);
try {
const RequestData = {
PlantId: PlantId,
Stage: Stage
};
if (Direction !== null) {
RequestData.Direction =
Direction;
}
await AdminPlantRequest(
"DeleteImage",
{
PlantId: PlantId,
Stage: Stage
}
RequestData
);
localStorage.removeItem(
@@ -1853,6 +2074,11 @@ async function DeleteAdminPlantImage(
SetAdminPlantMessage(
"Stage " +
Stage +
(
Direction === null
? ""
: " " + Direction
) +
" sprite removed."
);
} catch (Error) {
@@ -1888,6 +2114,8 @@ function GetAdminPlantPortableData(
Plant.Effects ?? {}
)
),
DirectionalSprites:
Plant.DirectionalSprites === true,
Shop: {
ShopPlant:
Plant.ShopPlant === true,
@@ -2060,6 +2288,9 @@ async function ImportAdminPlantJson(
)
)
: {},
DirectionalSprites:
Imported.DirectionalSprites ===
true,
ShopPlant:
Shop.ShopPlant === true ||
Imported.ShopPlant === true,
@@ -2120,6 +2351,11 @@ async function ImportAdminPlantJson(
"AdminPlantShopPlant"
).checked = Plant.ShopPlant;
document.getElementById(
"AdminPlantDirectionalSprites"
).checked =
Plant.DirectionalSprites;
SetAdminPlantField(
"AdminPlantBaseCost",
Plant.BaseCost ?? ""
+9
View File
@@ -205,6 +205,15 @@ function ValidateAdminPlant(
);
}
if (
Plant.DirectionalSprites !== undefined &&
typeof Plant.DirectionalSprites !== "boolean"
) {
throw new Error(
"Directional sprites must be On or Off."
);
}
if (Plant.ShopPlant === true) {
ValidateAdminInteger(
Number(Plant.BaseCost),
+302 -84
View File
@@ -26,6 +26,20 @@ const GardenBorderColours = [
"var(--Lavender)"
];
const GardenPlotDefaultRotation = "East";
const GardenPlotRotations = [
"North",
"East",
"South",
"West"
];
const GardenPlotRotationLabels = {
North: "^",
East: "->",
South: "v",
West: "<-"
};
function GetGardenBorderColour(
GardenIndex
@@ -1383,7 +1397,9 @@ function AreGardenPlotsEqual(
return (
A.Plant === B.Plant &&
Number(A.PlantedAt) ===
Number(B.PlantedAt)
Number(B.PlantedAt) &&
GetGardenPlotRotation(A) ===
GetGardenPlotRotation(B)
);
}
@@ -1405,44 +1421,135 @@ function GetGardenDisplaySettings() {
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 Button =
const Tile =
document.createElement(
"div"
);
Tile.className =
"PlantTile GardenPlot";
const MainButton =
document.createElement(
"button"
);
Button.className =
"PlantTile GardenPlot";
Button.type = "button";
MainButton.className =
"GardenPlotMain";
MainButton.type = "button";
if (
DisplaySettings.ShowPlantNames
) {
Button.classList.add(
Tile.classList.add(
"GardenPlotWithName"
);
}
if (
DisplaySettings.ShowGrowthTimers
) {
Button.classList.add(
"GardenPlotWithTimer"
);
}
const Plant =
Plot === null
@@ -1452,7 +1559,7 @@ function CreatePlotElement(
if (
DisplaySettings.ShowPlantNames
) {
Button.appendChild(
MainButton.appendChild(
CreateGardenPlotName(
Plot,
Plant
@@ -1469,43 +1576,45 @@ function CreatePlotElement(
Visual.className =
"GardenPlotVisual";
Button.appendChild(
MainButton.appendChild(
Visual
);
Tile.appendChild(
MainButton
);
if (Plot === null) {
Button.classList.add(
Tile.classList.add(
"GardenPlotEmpty"
);
Button.title =
MainButton.title =
IsPreview
? "Future Sight: empty plot"
: "Empty plot";
if (
DisplaySettings
.ShowGrowthTimers
) {
Button.appendChild(
CreateGardenPlotTimer(
null,
null,
0
)
);
}
AppendGardenPlotFooter(
Tile,
null,
null,
0,
PlotIndex,
DisplaySettings,
IsPreview
);
if (IsPreview) {
return Button;
MainButton.disabled = true;
return Tile;
}
if (
SelectedGardenTool ===
"Trowel"
) {
Button.addEventListener(
MainButton.addEventListener(
"click",
() => {
PlantSeed(
@@ -1517,40 +1626,40 @@ function CreatePlotElement(
SelectedGardenTool ===
"MagicTrowel"
) {
Button.classList.add(
Tile.classList.add(
"GardenPlotMagicPlant"
);
Button.title =
MainButton.title =
"Fill empty plots with the selected seed";
Button.addEventListener(
MainButton.addEventListener(
"click",
PlantManySeeds
);
} else {
MainButton.disabled = true;
}
return Button;
return Tile;
}
if (Plant === undefined) {
Visual.textContent = "?";
if (
DisplaySettings
.ShowGrowthTimers
) {
Button.appendChild(
CreateGardenPlotTimer(
Plot,
null,
0
)
);
}
AppendGardenPlotFooter(
Tile,
Plot,
null,
0,
PlotIndex,
DisplaySettings,
IsPreview
);
return Button;
MainButton.disabled = true;
return Tile;
}
@@ -1560,7 +1669,7 @@ function CreatePlotElement(
Plant
);
Button.style.setProperty(
Tile.style.setProperty(
"--GrowthProgress",
`${Progress * 100}%`
);
@@ -1608,25 +1717,24 @@ function CreatePlotElement(
}
if (
DisplaySettings.ShowGrowthTimers
) {
Button.appendChild(
CreateGardenPlotTimer(
Plot,
Plant,
Progress
)
);
}
AppendGardenPlotFooter(
Tile,
Plot,
Plant,
Progress,
PlotIndex,
DisplaySettings,
IsPreview
);
if (IsPreview) {
Button.title =
MainButton.title =
"Future Sight: " +
Plant.Name;
return Button;
MainButton.disabled = true;
return Tile;
}
@@ -1634,15 +1742,15 @@ function CreatePlotElement(
SelectedGardenTool ===
"Shovel"
) {
Button.classList.add(
Tile.classList.add(
"GardenPlotRemove"
);
Button.title =
MainButton.title =
"Remove " +
Plant.Name;
Button.addEventListener(
MainButton.addEventListener(
"click",
() => {
RemovePlant(
@@ -1651,7 +1759,7 @@ function CreatePlotElement(
}
);
return Button;
return Tile;
}
@@ -1660,18 +1768,19 @@ function CreatePlotElement(
"Fertilizer"
) {
if (Progress >= 1) {
Button.title =
MainButton.title =
Plant.Name +
" is already fully grown";
return Button;
MainButton.disabled = true;
return Tile;
}
Button.classList.add(
Tile.classList.add(
"GardenPlotFertilize"
);
Button.title =
MainButton.title =
"Fertilize " +
Plant.Name +
" (" +
@@ -1680,7 +1789,7 @@ function CreatePlotElement(
) +
" minutes)";
Button.addEventListener(
MainButton.addEventListener(
"click",
() => {
FertilizePlant(
@@ -1689,7 +1798,7 @@ function CreatePlotElement(
}
);
return Button;
return Tile;
}
@@ -1697,7 +1806,7 @@ function CreatePlotElement(
SelectedGardenTool !==
"Trowel"
) {
Button.title =
MainButton.title =
Plant.Name +
" - " +
Math.floor(
@@ -1705,22 +1814,23 @@ function CreatePlotElement(
) +
"% grown";
return Button;
MainButton.disabled = true;
return Tile;
}
if (
Progress >= 1
) {
Button.classList.add(
Tile.classList.add(
"GardenPlotMature"
);
Button.title =
MainButton.title =
"Harvest " +
Plant.Name;
Button.addEventListener(
MainButton.addEventListener(
"click",
() => {
HarvestPlant(
@@ -1729,17 +1839,114 @@ function CreatePlotElement(
}
);
} else {
Button.title =
MainButton.title =
Plant.Name +
" - " +
Math.floor(
Progress * 100
) +
"% grown";
MainButton.disabled = true;
}
return Button;
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);
}
@@ -1946,7 +2153,8 @@ async function PlantSeed(
PlotIndex
] = {
Plant: PlantKey,
PlantedAt: ActionTime
PlantedAt: ActionTime,
Rotation: GardenPlotDefaultRotation
};
@@ -2072,7 +2280,8 @@ async function PlantManySeeds() {
EmptyPlotIndexes[Index]
] = {
Plant: PlantKey,
PlantedAt: PlantedAt
PlantedAt: PlantedAt,
Rotation: GardenPlotDefaultRotation
};
}
@@ -2597,8 +2806,17 @@ function GetPlantImages(
Plot,
Plant
) {
const Direction =
Plot !== null &&
Plant?.DirectionalSprites === true
? GetGardenPlotRotation(
Plot
)
: null;
return GetPlantImageSources(
Plant ?? Plot.Plant
Plant ?? Plot.Plant,
Direction
);
}
+21
View File
@@ -768,6 +768,27 @@ function ApplyMutationResult(
continue;
}
const ExistingPlot =
SaveData.Garden.Plots[
PlotIndex
];
if (
NewPlot !== null &&
typeof NewPlot === "object" &&
typeof NewPlot.Rotation !==
"string"
) {
NewPlot.Rotation =
ExistingPlot !== null &&
typeof ExistingPlot ===
"object" &&
typeof ExistingPlot.Rotation ===
"string"
? ExistingPlot.Rotation
: "East";
}
SaveData.Garden.Plots[
PlotIndex
+81 -3
View File
@@ -1,5 +1,13 @@
let Plants = {};
const PlantDirectionCodes = {
North: "N",
East: "E",
South: "S",
West: "W"
};
async function FetchPlantsFromApi() {
const Response =
@@ -50,7 +58,8 @@ async function FetchPlantsFromApi() {
}
function GetPlantImageSources(
PlantOrKey
PlantOrKey,
Direction = null
) {
const PlantKey =
typeof PlantOrKey === "string"
@@ -86,10 +95,43 @@ function GetPlantImageSources(
? Plant.ImageStages
: [];
const DirectionCode =
typeof Direction === "string"
? (
PlantDirectionCodes[
Direction
] ??
Direction.toUpperCase()
)
: null;
const DirectionalStages =
Plant?.DirectionalSprites === true &&
DirectionCode !== null &&
Array.isArray(
Plant?.DirectionalImageStages
)
? Plant.DirectionalImageStages
.filter(
ImageStage =>
String(
ImageStage?.Direction ??
""
).toUpperCase() ===
DirectionCode
)
: [];
let HighestStage =
StaticImages.length;
for (const ImageStage of ApiStages) {
for (
const ImageStage
of [
...ApiStages,
...DirectionalStages
]
) {
const Stage = Number(
typeof ImageStage === "object" &&
ImageStage !== null
@@ -156,10 +198,46 @@ function GetPlantImageSources(
Query.toString();
}
for (
const ImageStage
of DirectionalStages
) {
const Stage = Number(
ImageStage?.Stage
);
if (
!Number.isInteger(Stage) ||
Stage < 1
) {
continue;
}
const Revision = Number(
ImageStage?.Revision ?? 0
);
const Query =
new URLSearchParams({
PlantId: String(Plant.Id),
Stage: String(Stage),
Direction: DirectionCode,
v: String(
Number.isFinite(Revision)
? Revision
: 0
)
});
Images[Stage - 1] =
ApiUrl +
"/PlantImage.php?" +
Query.toString();
}
return Images;
}
function GetPlantMatureImageSource(
PlantOrKey
) {
+62
View File
@@ -92,6 +92,10 @@ function BindGardenDisplayPreferences() {
"ToggleGrowthTimersButton",
"ShowGrowthTimers"
],
[
"TogglePlotRotationButton",
"ShowPlotRotation"
],
[
"ToggleNextHarvestButton",
"ShowNextHarvest"
@@ -150,6 +154,7 @@ function BindGardenDisplayPreferences() {
function RenderGardenDisplayPreferences() {
RenderToolSelectionPreferences();
RenderPlantInformationPreferences();
RenderPlotRotationPreference();
RenderGardenOverviewPreferences();
}
@@ -262,6 +267,44 @@ function RenderPlantInformationPreferences() {
}
function RenderPlotRotationPreference() {
const Button =
document.getElementById(
"TogglePlotRotationButton"
);
const Message =
document.getElementById(
"PlotRotationDisplayMessage"
);
if (
Button === null ||
Message === null
) {
return;
}
const IsOwned =
HasRotationUpgrade(
ProfileSave
);
SetPreferenceButtonState(
Button,
"Plot rotation",
ProfileSave.Preferences
.ShowPlotRotation !== false,
!IsOwned
);
Message.textContent =
IsOwned
? "Plot directions can be hidden without changing their saved rotation."
: "Unlock Plot rotation in the Shop to use this setting.";
}
function RenderGardenOverviewPreferences() {
const Settings = [
[
@@ -378,6 +421,10 @@ async function ToggleGardenDisplayPreference(
"ShowGrowthTimers"
];
const RotationPreferences = [
"ShowPlotRotation"
];
const GardenOverviewPreferences = [
"ShowNextHarvest",
"ShowGardenSize",
@@ -398,6 +445,11 @@ async function ToggleGardenDisplayPreference(
PreferenceName
);
const IsRotationPreference =
RotationPreferences.includes(
PreferenceName
);
const IsGardenOverviewPreference =
GardenOverviewPreferences.includes(
PreferenceName
@@ -407,6 +459,7 @@ async function ToggleGardenDisplayPreference(
if (
!IsToolSelectionPreference &&
!IsPlantInformationPreference &&
!IsRotationPreference &&
!IsGardenOverviewPreference
) {
return;
@@ -421,6 +474,15 @@ async function ToggleGardenDisplayPreference(
return;
}
if (
IsRotationPreference &&
!HasRotationUpgrade(
ProfileSave
)
) {
return;
}
if (
IsGardenOverviewPreference &&
!HasGardenOverviewUpgrade(
+26 -3
View File
@@ -15,7 +15,7 @@ const MaximumGardenNameLength = 32;
const StartingGardenWidth = 4;
const StartingGardenHeight = 4;
const CurrentSaveVersion = 6;
const CurrentSaveVersion = 7;
function CreateSimulationRandomState() {
@@ -274,6 +274,20 @@ function NormalizeGardenData(
Date.now()
);
const ValidRotations = [
"North",
"East",
"South",
"West"
];
Plot.Rotation =
ValidRotations.includes(
Plot.Rotation
)
? Plot.Rotation
: "East";
delete Plot.VisualVariant;
delete Plot.AddedTags;
}
@@ -571,7 +585,8 @@ function CreateNewSave() {
GardenOverview: false,
GardenEconomy: false,
MutationHints: false,
MutationHintsLevel: 0
MutationHintsLevel: 0,
Rotation: false
},
Preferences: {
@@ -584,7 +599,8 @@ function CreateNewSave() {
ShowGrowingPlots: true,
ShowReadyPlots: true,
SelectQuickBoughtPlant: true,
SelectTrowelWithInventoryPlant: true
SelectTrowelWithInventoryPlant: true,
ShowPlotRotation: true
},
Gardens: [
@@ -831,6 +847,9 @@ function NormalizeSaveData(
SaveData.Upgrades.MutationHints ===
true;
SaveData.Upgrades.Rotation =
SaveData.Upgrades.Rotation === true;
SaveData.Upgrades.MutationHintsLevel =
Math.max(
0,
@@ -888,6 +907,10 @@ function NormalizeSaveData(
SaveData.Preferences.SelectTrowelWithInventoryPlant !==
false;
SaveData.Preferences.ShowPlotRotation =
SaveData.Preferences.ShowPlotRotation !==
false;
const LegacyGarden =
SaveData.Garden ??
+161
View File
@@ -1114,6 +1114,7 @@ function RenderGardenUpgrades() {
"Row"
),
CreateNewGardenCard(),
CreateRotationUpgradeCard(),
CreatePlantInformationUpgradeCard(),
CreateGardenOverviewUpgradeCard(),
CreateGardenEconomyUpgradeCard(),
@@ -1377,6 +1378,117 @@ function CreateNewGardenCard() {
}
function CreateRotationUpgradeCard() {
const Cost =
GetRotationUpgradeCost();
const IsOwned =
HasRotationUpgrade(
ShopSave
);
const Card =
document.createElement(
"article"
);
Card.className =
"Panel ShopItem";
const Header =
document.createElement(
"div"
);
Header.className =
"PanelHeader PanelHeaderInset ShopItemHeader" +
(
IsOwned
? " ShopItemHeaderOwned"
: ""
);
const Name =
document.createElement(
"h3"
);
Name.textContent =
"Plot rotation";
Header.appendChild(Name);
const Description =
document.createElement(
"p"
);
Description.textContent =
"Rotate planted plots clockwise. Direction is stored per plot and can be shown beside the plot timer.";
const Details =
document.createElement(
"div"
);
Details.className =
"ShopItemDetails";
Details.append(
CreateShopStat(
"Current",
IsOwned
? "Rotation unlocked"
: "Rotation locked"
),
CreateShopStat(
"Next",
IsOwned
? "Purchased"
: "Rotation controls"
)
);
const BuyButton =
document.createElement(
"button"
);
BuyButton.className =
"ActionButton ShopBuyButton";
BuyButton.type = "button";
if (IsOwned) {
BuyButton.textContent =
"Purchased";
BuyButton.disabled = true;
} else {
BuyButton.textContent =
"Buy - " +
Cost.toLocaleString() +
" Dew";
BuyButton.disabled =
ShopSave.Currency.Dew < Cost ||
ShopPurchasePending;
BuyButton.addEventListener(
"click",
BuyRotationUpgrade
);
}
Card.append(
Header,
Description,
Details,
BuyButton
);
return Card;
}
function CreatePlantInformationUpgradeCard() {
const Cost =
GetPlantInformationUpgradeCost();
@@ -2854,6 +2966,55 @@ async function BuyNewGarden() {
}
async function BuyRotationUpgrade() {
if (
ShopPurchasePending ||
HasRotationUpgrade(
ShopSave
)
) {
return;
}
const Cost =
GetRotationUpgradeCost();
if (ShopSave.Currency.Dew < Cost) {
SetShopMessage(
"You don't have enough Dew for Plot rotation."
);
return;
}
ShopPurchasePending = true;
try {
ShopSave.Currency.Dew -= Cost;
ShopSave.Statistics.CurrencySpent.Dew +=
Cost;
UnlockRotation(
ShopSave
);
SetShopMessage(
"Bought Plot rotation for " +
Cost.toLocaleString() +
" Dew. Rotation controls are now visible on planted plots."
);
RenderShop();
await SaveGame(
ShopSave
);
} finally {
ShopPurchasePending = false;
RenderShop();
}
}
async function BuyPlantInformationUpgrade() {
if (
ShopPurchasePending ||
+29
View File
@@ -27,6 +27,7 @@ const MutationHintsUnlockCost = 1500;
const MutationHintsUpgradeCostMultiplier = 2;
const MutationHintsMaximumLevel = 1;
const MutationHintsMaximumVisible = 10;
const RotationUnlockCost = 1200;
const NewGardenBaseCost = 4000;
const NewGardenCostMultiplier = 1.5;
@@ -175,6 +176,34 @@ function AddNewGarden(
}
function HasRotationUpgrade(
SaveData
) {
return (
SaveData
?.Upgrades
?.Rotation === true
);
}
function GetRotationUpgradeCost() {
return RotationUnlockCost;
}
function UnlockRotation(
SaveData
) {
SaveData.Upgrades ??= {};
SaveData.Preferences ??= {};
SaveData.Upgrades.Rotation = true;
SaveData.Preferences.ShowPlotRotation =
true;
}
function HasPlantInformationUpgrade(
SaveData
) {
+74 -8
View File
@@ -634,10 +634,30 @@ input, button, textarea, select {
}
.GardenPlotWithName,
.GardenPlotWithTimer {
.GardenPlotWithFooter {
aspect-ratio: auto;
}
.GardenPlotMain {
display: grid;
width: 100%;
min-width: 0;
height: 100%;
padding: 0;
cursor: inherit;
color: inherit;
background: transparent;
border: 0;
border-radius: 0;
font: inherit;
appearance: none;
opacity: 1;
}
.GardenPlotMain:disabled {
cursor: default;
}
.GardenPlotVisual {
display: flex;
width: 100%;
@@ -650,7 +670,8 @@ input, button, textarea, select {
}
.GardenPlotName,
.GardenPlotTimer {
.GardenPlotTimer,
.GardenPlotRotationButton {
display: flex;
min-width: 0;
min-height: 1.5rem;
@@ -663,6 +684,10 @@ input, button, textarea, select {
font-size: 0.75rem;
line-height: 1;
text-align: center;
}
.GardenPlotName,
.GardenPlotTimer {
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -671,10 +696,41 @@ input, button, textarea, select {
border-bottom: 2px solid var(--Surface2);
}
.GardenPlotTimer {
.GardenPlotFooter {
display: grid;
grid-template-columns: 1fr;
min-width: 0;
background-color: var(--Mantle);
border-top: 2px solid var(--Surface2);
}
.GardenPlotFooterSplit {
grid-template-columns: minmax(0, 1fr) auto;
}
.GardenPlotRotationButton {
padding-inline: 0.45rem;
cursor: pointer;
border: 0;
border-radius: 0;
appearance: none;
}
.GardenPlotFooterSplit .GardenPlotRotationButton {
border-left: 2px solid var(--Surface2);
}
.GardenPlotRotationButton:hover {
color: var(--Sky);
background-color: var(--Surface0);
}
.GardenPlotRotationButton:focus-visible,
.GardenPlotMain:focus-visible {
outline: 3px solid var(--Yellow);
outline-offset: -3px;
}
.GardenPlotMature .GardenPlotTimer {
color: var(--Green);
}
@@ -686,11 +742,6 @@ input, button, textarea, select {
box-shadow: 2px 2px 0 var(--Crust);
}
.GardenPlot:focus-visible {
outline: 3px solid var(--Yellow);
outline-offset: 3px;
}
.GardenPlotEmpty {
--TileBackground: var(--Mantle);
}
@@ -2304,6 +2355,17 @@ input, button, textarea, select {
flex: 1 1 18rem;
}
.AdminPlantDirectionalImageUpload {
display: grid;
grid-template-columns: repeat( 2, minmax(0, 1fr) );
gap: 0.75rem;
margin-top: 0.75rem;
}
.AdminPlantDirectionalImageUpload .AdminEditorField {
margin: 0;
}
.AdminPlantImageNote {
color: var(--Overlay2);
}
@@ -2350,6 +2412,10 @@ input, button, textarea, select {
grid-template-columns: auto minmax(0, 1fr);
}
.AdminPlantDirectionalImageUpload {
grid-template-columns: 1fr;
}
.AdminPlantImageRemoveButton {
grid-column: 1 / -1;
width: 100%;