mirror of
https://git.srhtnin.garden/sarah/srhtnin.github.io.git
synced 2026-10-13 18:38:11 +02:00
859 lines
15 KiB
JavaScript
859 lines
15 KiB
JavaScript
const SaveKeyName = "SarahtoninGardenSaveKey";
|
|
const LocalSaveName = "SarahtoninGarden";
|
|
|
|
const ApiUrl = "https://api.srhtnin.garden";
|
|
|
|
const StartingDew = 15;
|
|
|
|
const StartingSeeds = {
|
|
RedRose: 1
|
|
};
|
|
|
|
|
|
function CreateNewSave() {
|
|
const SeedInventory = {};
|
|
const StartingPlants = [];
|
|
|
|
for (
|
|
const [
|
|
PlantKey,
|
|
Amount
|
|
]
|
|
of Object.entries(
|
|
StartingSeeds
|
|
)
|
|
) {
|
|
const Plant =
|
|
Plants[PlantKey];
|
|
|
|
if (Plant === undefined) {
|
|
continue;
|
|
}
|
|
|
|
SeedInventory[
|
|
String(Plant.Id)
|
|
] = Amount;
|
|
|
|
StartingPlants.push(
|
|
Plant.Id
|
|
);
|
|
}
|
|
|
|
|
|
return {
|
|
Version: 2,
|
|
Revision: 0,
|
|
LastSavedAt: Date.now(),
|
|
|
|
Currency: {
|
|
Dew: StartingDew
|
|
},
|
|
|
|
Statistics: {
|
|
CurrencyEarned: {
|
|
Dew: 0
|
|
},
|
|
|
|
CurrencySpent: {
|
|
Dew: 0
|
|
},
|
|
|
|
SeedsPurchased: 0
|
|
},
|
|
|
|
Inventory: {
|
|
Seeds: SeedInventory
|
|
},
|
|
|
|
Upgrades: {
|
|
PlotSize: 0,
|
|
PlantInformation: false,
|
|
GardenOverview: false
|
|
},
|
|
|
|
Preferences: {
|
|
ShowPlantNames: true,
|
|
ShowGrowthTimers: true,
|
|
ShowNextHarvest: true,
|
|
ShowGardenSize: true,
|
|
ShowEmptyPlots: true,
|
|
ShowPlantedPlots: true,
|
|
ShowGrowingPlots: true,
|
|
ShowReadyPlots: true
|
|
},
|
|
|
|
Garden: {
|
|
Width: 3,
|
|
Height: 3,
|
|
Plots: Array(9).fill(null)
|
|
},
|
|
|
|
Discoveries: {
|
|
Plants: StartingPlants,
|
|
Mutations: []
|
|
},
|
|
|
|
MutationCooldowns: {}
|
|
};
|
|
}
|
|
|
|
|
|
function NormalizeSaveData(
|
|
SaveData
|
|
) {
|
|
SaveData.Version = Math.max(
|
|
Number(
|
|
SaveData.Version ?? 1
|
|
),
|
|
2
|
|
);
|
|
|
|
SaveData.Revision = Number(
|
|
SaveData.Revision ?? 0
|
|
);
|
|
|
|
SaveData.LastSavedAt = Number(
|
|
SaveData.LastSavedAt ??
|
|
Date.now()
|
|
);
|
|
|
|
|
|
SaveData.Currency ??= {};
|
|
SaveData.Currency.Dew = Number(
|
|
SaveData.Currency.Dew ?? 0
|
|
);
|
|
|
|
|
|
SaveData.Statistics ??= {};
|
|
SaveData.Statistics.CurrencyEarned ??= {};
|
|
SaveData.Statistics.CurrencySpent ??= {};
|
|
|
|
SaveData.Statistics.CurrencyEarned.Dew ??=
|
|
SaveData.Currency.Dew;
|
|
|
|
SaveData.Statistics.CurrencySpent.Dew ??= 0;
|
|
SaveData.Statistics.SeedsPurchased ??= 0;
|
|
|
|
|
|
const HadSeedInventory =
|
|
SaveData.Inventory
|
|
?.Seeds !== undefined;
|
|
|
|
SaveData.Inventory ??= {};
|
|
SaveData.Inventory.Seeds ??= {};
|
|
|
|
|
|
/*
|
|
* Migrate the old infinite seed unlock list
|
|
* into the new finite seed inventory.
|
|
*/
|
|
|
|
for (
|
|
const LegacyPlant
|
|
of SaveData.Seeds
|
|
?.Unlocked ?? []
|
|
) {
|
|
const PlantId =
|
|
ResolveLegacyPlantId(
|
|
LegacyPlant
|
|
);
|
|
|
|
if (PlantId === null) {
|
|
continue;
|
|
}
|
|
|
|
SaveData.Inventory.Seeds[
|
|
String(PlantId)
|
|
] ??= 1;
|
|
}
|
|
|
|
|
|
/*
|
|
* Existing saves predate inventory, so give them
|
|
* one seed for each starting plant as a migration
|
|
* safety net. New saves already contain these.
|
|
*/
|
|
|
|
if (!HadSeedInventory) {
|
|
for (
|
|
const Plant
|
|
of GetStartingPlantDefinitions()
|
|
) {
|
|
SaveData.Inventory.Seeds[
|
|
String(Plant.Id)
|
|
] ??= 1;
|
|
}
|
|
}
|
|
|
|
|
|
for (
|
|
const InventoryKey
|
|
of Object.keys(
|
|
SaveData.Inventory.Seeds
|
|
)
|
|
) {
|
|
const Amount = Math.max(
|
|
0,
|
|
Math.floor(
|
|
Number(
|
|
SaveData.Inventory.Seeds[
|
|
InventoryKey
|
|
] ?? 0
|
|
)
|
|
)
|
|
);
|
|
|
|
SaveData.Inventory.Seeds[
|
|
InventoryKey
|
|
] = Amount;
|
|
}
|
|
|
|
|
|
delete SaveData.Seeds;
|
|
|
|
|
|
SaveData.Discoveries ??= {};
|
|
|
|
SaveData.Discoveries.Plants =
|
|
NormalizePlantDiscoveries(
|
|
SaveData.Discoveries.Plants ?? []
|
|
);
|
|
|
|
SaveData.Discoveries.Mutations =
|
|
NormalizeMutationDiscoveries(
|
|
SaveData.Discoveries.Mutations ?? []
|
|
);
|
|
|
|
|
|
for (
|
|
const Plant
|
|
of GetStartingPlantDefinitions()
|
|
) {
|
|
if (
|
|
!SaveData.Discoveries.Plants
|
|
.includes(
|
|
Plant.Id
|
|
)
|
|
) {
|
|
SaveData.Discoveries.Plants.push(
|
|
Plant.Id
|
|
);
|
|
}
|
|
}
|
|
|
|
SaveData.Discoveries.Plants.sort(
|
|
(A, B) => A - B
|
|
);
|
|
|
|
SaveData.Discoveries.Mutations.sort(
|
|
(A, B) => A - B
|
|
);
|
|
|
|
|
|
SaveData.Upgrades ??= {};
|
|
SaveData.Upgrades.PlotSize ??= 0;
|
|
SaveData.Upgrades.PlantInformation =
|
|
SaveData.Upgrades.PlantInformation ===
|
|
true;
|
|
|
|
SaveData.Upgrades.GardenOverview =
|
|
SaveData.Upgrades.GardenOverview ===
|
|
true;
|
|
|
|
|
|
SaveData.Preferences ??= {};
|
|
|
|
SaveData.Preferences.ShowPlantNames =
|
|
SaveData.Preferences.ShowPlantNames !==
|
|
false;
|
|
|
|
SaveData.Preferences.ShowGrowthTimers =
|
|
SaveData.Preferences.ShowGrowthTimers !==
|
|
false;
|
|
|
|
SaveData.Preferences.ShowNextHarvest =
|
|
SaveData.Preferences.ShowNextHarvest !==
|
|
false;
|
|
|
|
SaveData.Preferences.ShowGardenSize =
|
|
SaveData.Preferences.ShowGardenSize !==
|
|
false;
|
|
|
|
SaveData.Preferences.ShowEmptyPlots =
|
|
SaveData.Preferences.ShowEmptyPlots !==
|
|
false;
|
|
|
|
SaveData.Preferences.ShowPlantedPlots =
|
|
SaveData.Preferences.ShowPlantedPlots !==
|
|
false;
|
|
|
|
SaveData.Preferences.ShowGrowingPlots =
|
|
SaveData.Preferences.ShowGrowingPlots !==
|
|
false;
|
|
|
|
SaveData.Preferences.ShowReadyPlots =
|
|
SaveData.Preferences.ShowReadyPlots !==
|
|
false;
|
|
|
|
|
|
SaveData.Garden ??= {
|
|
Width: 3,
|
|
Height: 3,
|
|
Plots: []
|
|
};
|
|
|
|
SaveData.Garden.Width = Math.max(
|
|
1,
|
|
Math.floor(
|
|
Number(
|
|
SaveData.Garden.Width ?? 3
|
|
) || 3
|
|
)
|
|
);
|
|
|
|
SaveData.Garden.Height = Math.max(
|
|
1,
|
|
Math.floor(
|
|
Number(
|
|
SaveData.Garden.Height ?? 3
|
|
) || 3
|
|
)
|
|
);
|
|
|
|
SaveData.Garden.Plots ??= [];
|
|
|
|
|
|
for (
|
|
let PlotIndex = 0;
|
|
PlotIndex <
|
|
SaveData.Garden.Plots.length;
|
|
PlotIndex++
|
|
) {
|
|
const Plot =
|
|
SaveData.Garden.Plots[
|
|
PlotIndex
|
|
];
|
|
|
|
if (Plot === null) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
Plot.Plant === "Rose"
|
|
) {
|
|
Plot.Plant =
|
|
"RedRose";
|
|
}
|
|
|
|
if (
|
|
typeof Plot.Plant ===
|
|
"number"
|
|
) {
|
|
const PlantKey =
|
|
ResolvePlantKeyById(
|
|
Plot.Plant
|
|
);
|
|
|
|
if (PlantKey !== null) {
|
|
Plot.Plant = PlantKey;
|
|
}
|
|
}
|
|
|
|
Plot.PlantedAt = Number(
|
|
Plot.PlantedAt ??
|
|
Date.now()
|
|
);
|
|
|
|
delete Plot.VisualVariant;
|
|
delete Plot.AddedTags;
|
|
}
|
|
|
|
|
|
const RequiredPlots =
|
|
SaveData.Garden.Width *
|
|
SaveData.Garden.Height;
|
|
|
|
while (
|
|
SaveData.Garden.Plots.length <
|
|
RequiredPlots
|
|
) {
|
|
SaveData.Garden.Plots.push(null);
|
|
}
|
|
|
|
if (
|
|
SaveData.Garden.Plots.length >
|
|
RequiredPlots
|
|
) {
|
|
SaveData.Garden.Plots.length =
|
|
RequiredPlots;
|
|
}
|
|
|
|
|
|
SaveData.MutationCooldowns ??= {};
|
|
|
|
|
|
return SaveData;
|
|
}
|
|
|
|
|
|
function GetStartingPlantDefinitions() {
|
|
const StartingPlants = [];
|
|
|
|
for (
|
|
const PlantKey
|
|
of Object.keys(
|
|
StartingSeeds
|
|
)
|
|
) {
|
|
const Plant =
|
|
Plants[PlantKey];
|
|
|
|
if (Plant !== undefined) {
|
|
StartingPlants.push(
|
|
Plant
|
|
);
|
|
}
|
|
}
|
|
|
|
return StartingPlants;
|
|
}
|
|
|
|
|
|
function ResolveLegacyPlantId(
|
|
Value
|
|
) {
|
|
if (
|
|
typeof Value ===
|
|
"number"
|
|
) {
|
|
return Value;
|
|
}
|
|
|
|
if (
|
|
typeof Value !==
|
|
"string"
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const LegacyPlantIds = {
|
|
Rose: 1,
|
|
RedRose: 1,
|
|
DarkRose: 2,
|
|
BlueRose: 3,
|
|
PurpleRose: 4
|
|
};
|
|
|
|
if (
|
|
LegacyPlantIds[Value] !==
|
|
undefined
|
|
) {
|
|
return LegacyPlantIds[Value];
|
|
}
|
|
|
|
if (
|
|
typeof Plants !==
|
|
"undefined" &&
|
|
Plants[Value] !== undefined
|
|
) {
|
|
return Plants[Value].Id;
|
|
}
|
|
|
|
const NumericValue =
|
|
Number(Value);
|
|
|
|
if (
|
|
Number.isInteger(
|
|
NumericValue
|
|
) &&
|
|
NumericValue > 0
|
|
) {
|
|
return NumericValue;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
function ResolvePlantKeyById(
|
|
PlantId
|
|
) {
|
|
if (
|
|
typeof Plants ===
|
|
"undefined"
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
for (
|
|
const [PlantKey, Plant]
|
|
of Object.entries(Plants)
|
|
) {
|
|
if (
|
|
Plant.Id ===
|
|
Number(PlantId)
|
|
) {
|
|
return PlantKey;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
function NormalizePlantDiscoveries(
|
|
Values
|
|
) {
|
|
const PlantIds = [];
|
|
|
|
for (
|
|
const Value
|
|
of Values
|
|
) {
|
|
const PlantId =
|
|
ResolveLegacyPlantId(
|
|
Value
|
|
);
|
|
|
|
if (
|
|
PlantId !== null &&
|
|
!PlantIds.includes(
|
|
PlantId
|
|
)
|
|
) {
|
|
PlantIds.push(
|
|
PlantId
|
|
);
|
|
}
|
|
}
|
|
|
|
return PlantIds;
|
|
}
|
|
|
|
|
|
function NormalizeMutationDiscoveries(
|
|
Values
|
|
) {
|
|
const MutationIds = [];
|
|
|
|
for (
|
|
const Value
|
|
of Values
|
|
) {
|
|
let MutationId = null;
|
|
|
|
if (
|
|
typeof Value ===
|
|
"number"
|
|
) {
|
|
MutationId = Value;
|
|
} else if (
|
|
typeof Value ===
|
|
"string" &&
|
|
typeof MutationSets !==
|
|
"undefined" &&
|
|
MutationSets[Value] !==
|
|
undefined
|
|
) {
|
|
MutationId =
|
|
MutationSets[Value].Id;
|
|
} else {
|
|
const NumericValue =
|
|
Number(Value);
|
|
|
|
if (
|
|
Number.isInteger(
|
|
NumericValue
|
|
) &&
|
|
NumericValue > 0
|
|
) {
|
|
MutationId =
|
|
NumericValue;
|
|
}
|
|
}
|
|
|
|
if (
|
|
MutationId !== null &&
|
|
!MutationIds.includes(
|
|
MutationId
|
|
)
|
|
) {
|
|
MutationIds.push(
|
|
MutationId
|
|
);
|
|
}
|
|
}
|
|
|
|
return MutationIds;
|
|
}
|
|
|
|
|
|
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 NormalizeSaveData(
|
|
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(
|
|
SaveKey = GetSaveKey()
|
|
) {
|
|
const Response = await fetch(
|
|
ApiUrl + "/Load.php",
|
|
{
|
|
method: "POST",
|
|
|
|
headers: {
|
|
"Content-Type":
|
|
"application/json"
|
|
},
|
|
|
|
body: JSON.stringify({
|
|
SaveKey: SaveKey
|
|
})
|
|
}
|
|
);
|
|
|
|
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 NormalizeSaveData(
|
|
Result.SaveData
|
|
);
|
|
}
|
|
|
|
|
|
async function SaveGame(SaveData) {
|
|
NormalizeSaveData(
|
|
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;
|
|
}
|
|
|
|
WriteLocalSave(LocalSave);
|
|
|
|
return LocalSave;
|
|
}
|