Added gardening minigame.

This commit is contained in:
sarah committed 2026-08-30 15:36:16 +02:00
1 parent 3efbb10fa5
commit 660d09272d
9 files changed
+986

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

+53
View File
@@ -0,0 +1,53 @@
<?php
$PageTitle = "Garden";
?>
<!DOCTYPE html>
<html lang="en" data-palette="CatppuccinMocha">
<?php require __DIR__ . "/Bits/Head.php"; ?>
<body>
<?php require __DIR__ . "/Bits/Navbar.php"; ?>
<main class="MainContainer">
<h1>Garden</h1>
<div class="GardenStatus">
<strong>Dew:</strong>
<span id="DewAmount">0</span>
</div>
<div class="GardenLayout">
<section>
<h2>Plots</h2>
<div
id="GardenGrid"
class="GardenGrid"
></div>
</section>
<section class="SeedPanel">
<h2>Seeds</h2>
<div
id="SeedList"
class="SeedList"
></div>
</section>
</div>
<p id="GardenMessage">
Select a seed, then choose an empty plot.
</p>
</main>
<script src="/Scripts/Garden/Plants.js"></script>
<script src="/Scripts/Garden/Mutations.js"></script>
<script src="/Scripts/Garden/Save.js"></script>
<script src="/Scripts/Garden/Garden.js"></script>
</body>
</html>
+558
View File
@@ -0,0 +1,558 @@
let GameSave;
let SelectedSeed = "Rose";
const GardenUpdateInterval = 1000;
async function StartGame() {
GameSave = await LoadGame();
NormalizeSave();
RenderGame();
setInterval(
RenderGarden,
GardenUpdateInterval
);
}
function NormalizeSave() {
GameSave.Currency ??= {};
GameSave.Currency.Dew ??= 0;
GameSave.Seeds ??= {};
GameSave.Seeds.Unlocked ??= [
"Rose"
];
GameSave.Garden ??= {
Width: 3,
Height: 3,
Plots: []
};
const RequiredPlots =
GameSave.Garden.Width *
GameSave.Garden.Height;
while (
GameSave.Garden.Plots.length <
RequiredPlots
) {
GameSave.Garden.Plots.push(null);
}
}
function RenderGame() {
RenderCurrency();
RenderSeeds();
RenderGarden();
}
function RenderCurrency() {
const DewAmount =
document.getElementById(
"DewAmount"
);
DewAmount.textContent =
GameSave.Currency.Dew;
}
function RenderSeeds() {
const SeedList =
document.getElementById(
"SeedList"
);
SeedList.replaceChildren();
for (
const PlantId
of GameSave.Seeds.Unlocked
) {
const Plant =
Plants[PlantId];
if (Plant === undefined) {
continue;
}
const Button =
document.createElement(
"button"
);
Button.className =
"SeedButton";
Button.type = "button";
Button.textContent =
Plant.Name;
if (
PlantId ===
SelectedSeed
) {
Button.setAttribute(
"aria-current",
"true"
);
}
Button.addEventListener(
"click",
() => {
SelectedSeed =
PlantId;
RenderSeeds();
SetGardenMessage(
"Selected " +
Plant.Name +
"."
);
}
);
SeedList.appendChild(
Button
);
}
}
function RenderGarden() {
const GardenGrid =
document.getElementById(
"GardenGrid"
);
GardenGrid.style.setProperty(
"--GardenWidth",
GameSave.Garden.Width
);
GardenGrid.replaceChildren();
GameSave.Garden.Plots.forEach(
(Plot, PlotIndex) => {
const PlotElement =
CreatePlotElement(
Plot,
PlotIndex
);
GardenGrid.appendChild(
PlotElement
);
}
);
}
function CreatePlotElement(
Plot,
PlotIndex
) {
const Button =
document.createElement(
"button"
);
Button.className =
"GardenPlot";
Button.type = "button";
if (Plot === null) {
Button.classList.add(
"GardenPlotEmpty"
);
Button.title =
"Empty plot";
Button.addEventListener(
"click",
() => {
PlantSeed(
PlotIndex
);
}
);
return Button;
}
const Plant =
Plants[Plot.Plant];
if (Plant === undefined) {
Button.textContent = "?";
return Button;
}
const Stage =
GetPlantStage(
Plot,
Plant
);
const Image =
document.createElement(
"img"
);
Image.className =
"GardenPlant";
Image.alt =
Plant.Name +
" - " +
Stage;
SetPlantImage(
Image,
Plot,
Plant,
Stage
);
Button.appendChild(
Image
);
if (Stage === "Mature") {
Button.classList.add(
"GardenPlotMature"
);
Button.title =
"Harvest " +
Plant.Name;
Button.addEventListener(
"click",
() => {
HarvestPlant(
PlotIndex
);
}
);
} else {
const Progress =
GetPlantGrowthProgress(
Plot,
Plant
);
Button.title =
Plant.Name +
" - " +
Math.floor(
Progress * 100
) +
"% grown";
}
return Button;
}
async function PlantSeed(
PlotIndex
) {
if (
GameSave.Garden.Plots[
PlotIndex
] !== null
) {
return;
}
if (
!GameSave.Seeds.Unlocked
.includes(
SelectedSeed
)
) {
return;
}
GameSave.Garden.Plots[
PlotIndex
] = {
Plant: SelectedSeed,
AddedTags: [],
VisualVariant: null,
PlantedAt: Date.now()
};
SetGardenMessage(
"Planted " +
Plants[SelectedSeed].Name +
"."
);
RenderGarden();
await SaveGame(
GameSave
);
}
async function HarvestPlant(
PlotIndex
) {
const Plot =
GameSave.Garden.Plots[
PlotIndex
];
if (Plot === null) {
return;
}
const Plant =
Plants[Plot.Plant];
if (
GetPlantStage(
Plot,
Plant
) !== "Mature"
) {
return;
}
GiveReward(
Plant.Reward
);
GameSave.Garden.Plots[
PlotIndex
] = null;
SetGardenMessage(
"Harvested " +
Plant.Name +
" for " +
Plant.Reward.Amount +
" " +
Plant.Reward.Currency +
"."
);
RenderGame();
await SaveGame(
GameSave
);
}
function GiveReward(
Reward
) {
GameSave.Currency[
Reward.Currency
] ??= 0;
GameSave.Currency[
Reward.Currency
] += Reward.Amount;
}
function GetPlantGrowthProgress(
Plot,
Plant
) {
if (
Plant.GrowthTime <= 0
) {
return 1;
}
const Age =
Date.now() -
Plot.PlantedAt;
return Math.min(
Age /
Plant.GrowthTime,
1
);
}
function GetPlantStage(
Plot,
Plant
) {
const Progress =
GetPlantGrowthProgress(
Plot,
Plant
);
if (Progress < 1 / 3) {
return "Seed";
}
if (Progress < 2 / 3) {
return "Sprout";
}
return "Mature";
}
function GetPlantTags(
Plot,
Plant
) {
return [
...Plant.Tags,
...(Plot.AddedTags ?? [])
];
}
function GetPlantVariant(
Plot,
Plant
) {
if (
Plot.VisualVariant !==
null
) {
return Plot.VisualVariant;
}
const Tags =
GetPlantTags(
Plot,
Plant
);
const Variants = [
...Plant.Variants
].sort(
(A, B) =>
B.Priority -
A.Priority
);
for (
const Variant
of Variants
) {
const Matches =
Variant.Tags.every(
Tag =>
Tags.includes(
Tag
)
);
if (Matches) {
return Variant.Name;
}
}
return "Default";
}
function SetPlantImage(
Image,
Plot,
Plant,
Stage
) {
const Variant =
GetPlantVariant(
Plot,
Plant
);
const BasePath =
"/Assets/Img/Garden/Plants/" +
Plot.Plant +
"/" +
Variant +
"/";
/*
* Try the requested stage first.
*
* If that image doesn't exist,
* Mature.png becomes the fallback.
*/
const Candidates = [
BasePath + Stage + ".png",
BasePath + "Mature.png"
];
let CandidateIndex = 0;
Image.src =
Candidates[
CandidateIndex
];
Image.addEventListener(
"error",
() => {
CandidateIndex++;
if (
CandidateIndex <
Candidates.length
) {
Image.src =
Candidates[
CandidateIndex
];
return;
}
Image.remove();
}
);
}
function SetGardenMessage(
Message
) {
document.getElementById(
"GardenMessage"
).textContent = Message;
}
document.addEventListener(
"DOMContentLoaded",
StartGame
);
+40
View File
@@ -0,0 +1,40 @@
const MutationSets = {
DarkRose: {
Name: "Dark Rose",
Chance: 0.1,
Cooldown: 120000,
Pattern: [
[
{
Tags: ["Rose"],
Capture: "ParentA"
},
"Empty"
],
[
"Empty",
{
Tags: ["Rose"],
Capture: "ParentB"
}
]
],
Success: [
[
"Empty",
"DarkRose"
],
[
"DarkRose",
"Empty"
]
],
Failure: "Keep"
}
};
+36
View File
@@ -0,0 +1,36 @@
const Plants = {
Rose: {
Name: "Rose",
Tags: [
"Rose",
"Flower",
"Red"
],
GrowthTime: 60000,
Variants: [
{
Name: "Red",
Tags: [
"Red"
],
Priority: 10
},
{
Name: "Default",
Tags: [],
Priority: 0
}
],
Effects: [],
Reward: {
Currency: "Dew",
Amount: 5
}
}
};
+299
View File
@@ -0,0 +1,299 @@
const SaveKeyName = "SarahtoninGardenSaveKey";
const LocalSaveName = "SarahtoninGarden";
const ApiUrl = "https://api.srhtnin.garden";
function CreateNewSave() {
return {
Version: 1,
Revision: 0,
LastSavedAt: Date.now(),
Currency: {
Dew: 0
},
Seeds: {
Unlocked: [
"Rose"
]
},
Upgrades: {
PlotSize: 0
},
Garden: {
Width: 3,
Height: 3,
Plots: Array(9).fill(null)
},
Discoveries: {
Plants: [
"Rose"
],
Mutations: []
},
MutationCooldowns: {}
};
}
function GetSaveKey() {
let SaveKey = localStorage.getItem(
SaveKeyName
);
if (SaveKey !== null) {
return SaveKey;
}
const Bytes = new Uint8Array(32);
crypto.getRandomValues(Bytes);
SaveKey = Array.from(
Bytes,
Byte => Byte
.toString(16)
.padStart(2, "0")
).join("");
localStorage.setItem(
SaveKeyName,
SaveKey
);
return SaveKey;
}
function WriteLocalSave(SaveData) {
localStorage.setItem(
LocalSaveName,
JSON.stringify(SaveData)
);
}
function ReadLocalSave() {
const RawSave = localStorage.getItem(
LocalSaveName
);
if (RawSave === null) {
return null;
}
try {
return JSON.parse(RawSave);
} catch (Error) {
console.error(
"Couldn't read local save:",
Error
);
return null;
}
}
async function WriteRemoteSave(SaveData) {
const Response = await fetch(
ApiUrl + "/Save.php",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
SaveKey: GetSaveKey(),
SaveData: SaveData
})
}
);
if (!Response.ok) {
throw new Error(
"Save API returned HTTP " +
Response.status
);
}
const Result = await Response.json();
if (!Result.Success) {
throw new Error(
Result.Error ??
"Remote save failed."
);
}
return Result;
}
async function ReadRemoteSave() {
const Response = await fetch(
ApiUrl + "/Load.php",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
SaveKey: GetSaveKey()
})
}
);
if (!Response.ok) {
throw new Error(
"Load API returned HTTP " +
Response.status
);
}
const Result = await Response.json();
if (!Result.Success) {
throw new Error(
Result.Error ??
"Remote load failed."
);
}
if (!Result.Exists) {
return null;
}
return Result.SaveData;
}
async function SaveGame(SaveData) {
SaveData.Revision++;
SaveData.LastSavedAt = Date.now();
WriteLocalSave(SaveData);
try {
await WriteRemoteSave(SaveData);
} catch (Error) {
console.error(
"Couldn't synchronize garden save:",
Error
);
}
}
async function LoadGame() {
const LocalSave = ReadLocalSave();
let RemoteSave = null;
try {
RemoteSave = await ReadRemoteSave();
} catch (Error) {
console.error(
"Couldn't retrieve remote save:",
Error
);
}
if (
LocalSave === null &&
RemoteSave === null
) {
const NewSave = CreateNewSave();
WriteLocalSave(NewSave);
try {
await WriteRemoteSave(NewSave);
} catch (Error) {
console.error(
"Couldn't create remote save:",
Error
);
}
return NewSave;
}
if (
LocalSave !== null &&
RemoteSave === null
) {
try {
await WriteRemoteSave(LocalSave);
} catch (Error) {
console.error(
"Couldn't upload local save:",
Error
);
}
return LocalSave;
}
if (
LocalSave === null &&
RemoteSave !== null
) {
WriteLocalSave(RemoteSave);
return RemoteSave;
}
if (
LocalSave.Revision >
RemoteSave.Revision
) {
try {
await WriteRemoteSave(LocalSave);
} catch (Error) {
console.error(
"Couldn't update remote save:",
Error
);
}
return LocalSave;
}
if (
RemoteSave.Revision >
LocalSave.Revision
) {
WriteLocalSave(RemoteSave);
return RemoteSave;
}
if (
RemoteSave.LastSavedAt >
LocalSave.LastSavedAt
) {
WriteLocalSave(RemoteSave);
return RemoteSave;
}
return LocalSave;
}