mirror of
https://git.srhtnin.garden/sarah/srhtnin.github.io.git
synced 2026-10-13 18:38:11 +02:00
Added gardening minigame.
This commit is contained in:
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 |
@@ -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>
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user