mirror of
https://git.srhtnin.garden/sarah/srhtnin.github.io.git
synced 2026-10-14 10:48:12 +02:00
Added shop, inventory, fixed plant and mutation definitions, and more..
This commit is contained in:
1 parent
cd2759ce4e
commit
97204f2d3f
15 files changed
+2510
-351
No files matched your search
@@ -0,0 +1,578 @@
|
||||
const MutationTimeFeePerHour = 10;
|
||||
|
||||
const PlantKeysById = new Map();
|
||||
const PlantsById = new Map();
|
||||
const MutationsById = new Map();
|
||||
|
||||
|
||||
for (
|
||||
const [PlantKey, Plant]
|
||||
of Object.entries(Plants)
|
||||
) {
|
||||
if (PlantsById.has(Plant.Id)) {
|
||||
throw new Error(
|
||||
"Duplicate plant ID: " +
|
||||
Plant.Id
|
||||
);
|
||||
}
|
||||
|
||||
PlantKeysById.set(
|
||||
Plant.Id,
|
||||
PlantKey
|
||||
);
|
||||
|
||||
PlantsById.set(
|
||||
Plant.Id,
|
||||
Plant
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
for (
|
||||
const Mutation
|
||||
of Object.values(MutationSets)
|
||||
) {
|
||||
if (MutationsById.has(Mutation.Id)) {
|
||||
throw new Error(
|
||||
"Duplicate mutation ID: " +
|
||||
Mutation.Id
|
||||
);
|
||||
}
|
||||
|
||||
MutationsById.set(
|
||||
Mutation.Id,
|
||||
Mutation
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function GetPlantKeyById(
|
||||
PlantId
|
||||
) {
|
||||
return PlantKeysById.get(
|
||||
Number(PlantId)
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
|
||||
function GetPlantById(
|
||||
PlantId
|
||||
) {
|
||||
return PlantsById.get(
|
||||
Number(PlantId)
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
|
||||
function GetMutationById(
|
||||
MutationId
|
||||
) {
|
||||
return MutationsById.get(
|
||||
Number(MutationId)
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
|
||||
function HasDiscoveredPlant(
|
||||
SaveData,
|
||||
PlantId
|
||||
) {
|
||||
return SaveData.Discoveries.Plants.includes(
|
||||
Number(PlantId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function HasDiscoveredMutation(
|
||||
SaveData,
|
||||
MutationId
|
||||
) {
|
||||
return SaveData.Discoveries.Mutations.includes(
|
||||
Number(MutationId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function DiscoverPlant(
|
||||
SaveData,
|
||||
PlantId
|
||||
) {
|
||||
PlantId = Number(PlantId);
|
||||
|
||||
if (
|
||||
!HasDiscoveredPlant(
|
||||
SaveData,
|
||||
PlantId
|
||||
)
|
||||
) {
|
||||
SaveData.Discoveries.Plants.push(
|
||||
PlantId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function DiscoverMutation(
|
||||
SaveData,
|
||||
MutationId
|
||||
) {
|
||||
MutationId = Number(
|
||||
MutationId
|
||||
);
|
||||
|
||||
if (
|
||||
!HasDiscoveredMutation(
|
||||
SaveData,
|
||||
MutationId
|
||||
)
|
||||
) {
|
||||
SaveData.Discoveries.Mutations.push(
|
||||
MutationId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function IsPlantAvailableInShop(
|
||||
SaveData,
|
||||
Plant
|
||||
) {
|
||||
if (
|
||||
Plant.Shop?.StartingPlant ===
|
||||
true
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return HasDiscoveredPlant(
|
||||
SaveData,
|
||||
Plant.Id
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function GetSeedCount(
|
||||
SaveData,
|
||||
PlantId
|
||||
) {
|
||||
return Number(
|
||||
SaveData.Inventory.Seeds[
|
||||
String(PlantId)
|
||||
] ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function AddSeed(
|
||||
SaveData,
|
||||
PlantId,
|
||||
Amount = 1
|
||||
) {
|
||||
const InventoryKey =
|
||||
String(PlantId);
|
||||
|
||||
SaveData.Inventory.Seeds[
|
||||
InventoryKey
|
||||
] ??= 0;
|
||||
|
||||
SaveData.Inventory.Seeds[
|
||||
InventoryKey
|
||||
] += Amount;
|
||||
}
|
||||
|
||||
|
||||
function TakeSeed(
|
||||
SaveData,
|
||||
PlantId,
|
||||
Amount = 1
|
||||
) {
|
||||
const CurrentAmount =
|
||||
GetSeedCount(
|
||||
SaveData,
|
||||
PlantId
|
||||
);
|
||||
|
||||
if (CurrentAmount < Amount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SaveData.Inventory.Seeds[
|
||||
String(PlantId)
|
||||
] = CurrentAmount - Amount;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function GetPlantShopCost(
|
||||
SaveData,
|
||||
PlantId,
|
||||
Cache = new Map(),
|
||||
Visiting = new Set()
|
||||
) {
|
||||
PlantId = Number(PlantId);
|
||||
|
||||
if (Cache.has(PlantId)) {
|
||||
return Cache.get(PlantId);
|
||||
}
|
||||
|
||||
const Plant =
|
||||
GetPlantById(
|
||||
PlantId
|
||||
);
|
||||
|
||||
if (Plant === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
Plant.Shop?.StartingPlant ===
|
||||
true
|
||||
) {
|
||||
const BaseCost = Number(
|
||||
Plant.Shop.BaseCost
|
||||
);
|
||||
|
||||
if (
|
||||
!Number.isFinite(BaseCost) ||
|
||||
BaseCost < 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Cost =
|
||||
Math.ceil(BaseCost);
|
||||
|
||||
Cache.set(
|
||||
PlantId,
|
||||
Cost
|
||||
);
|
||||
|
||||
return Cost;
|
||||
}
|
||||
|
||||
if (
|
||||
!HasDiscoveredPlant(
|
||||
SaveData,
|
||||
PlantId
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Visiting.has(PlantId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Visiting.add(
|
||||
PlantId
|
||||
);
|
||||
|
||||
let CheapestCost = null;
|
||||
|
||||
|
||||
for (
|
||||
const Mutation
|
||||
of Object.values(
|
||||
MutationSets
|
||||
)
|
||||
) {
|
||||
if (
|
||||
!HasDiscoveredMutation(
|
||||
SaveData,
|
||||
Mutation.Id
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!Mutation.Relations
|
||||
?.PlantsCreated
|
||||
?.includes(
|
||||
PlantId
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const IngredientCost =
|
||||
GetMutationIngredientCost(
|
||||
SaveData,
|
||||
Mutation,
|
||||
Cache,
|
||||
Visiting
|
||||
);
|
||||
|
||||
if (IngredientCost === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const MutationCost =
|
||||
IngredientCost +
|
||||
GetMutationTimeFee(
|
||||
Mutation
|
||||
);
|
||||
|
||||
if (
|
||||
CheapestCost === null ||
|
||||
MutationCost < CheapestCost
|
||||
) {
|
||||
CheapestCost =
|
||||
MutationCost;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Visiting.delete(
|
||||
PlantId
|
||||
);
|
||||
|
||||
Cache.set(
|
||||
PlantId,
|
||||
CheapestCost
|
||||
);
|
||||
|
||||
return CheapestCost;
|
||||
}
|
||||
|
||||
|
||||
function GetMutationIngredientCost(
|
||||
SaveData,
|
||||
Mutation,
|
||||
Cache,
|
||||
Visiting
|
||||
) {
|
||||
let TotalCost = 0;
|
||||
|
||||
|
||||
for (
|
||||
const Row
|
||||
of Mutation.Pattern ?? []
|
||||
) {
|
||||
for (
|
||||
const Matcher
|
||||
of Row
|
||||
) {
|
||||
if (
|
||||
Matcher === null ||
|
||||
Matcher === "Empty"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const MatcherCost =
|
||||
GetMatcherSeedCost(
|
||||
SaveData,
|
||||
Matcher,
|
||||
Cache,
|
||||
Visiting
|
||||
);
|
||||
|
||||
if (MatcherCost === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TotalCost +=
|
||||
MatcherCost;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return TotalCost;
|
||||
}
|
||||
|
||||
|
||||
function GetMatcherSeedCost(
|
||||
SaveData,
|
||||
Matcher,
|
||||
Cache,
|
||||
Visiting
|
||||
) {
|
||||
if (typeof Matcher === "string") {
|
||||
const Plant =
|
||||
Plants[Matcher];
|
||||
|
||||
if (Plant === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetPlantShopCost(
|
||||
SaveData,
|
||||
Plant.Id,
|
||||
Cache,
|
||||
Visiting
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
typeof Matcher !== "object" ||
|
||||
Matcher === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
typeof Matcher.Plant ===
|
||||
"string"
|
||||
) {
|
||||
const Plant =
|
||||
Plants[Matcher.Plant];
|
||||
|
||||
if (Plant === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetPlantShopCost(
|
||||
SaveData,
|
||||
Plant.Id,
|
||||
Cache,
|
||||
Visiting
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
let CheapestCost = null;
|
||||
|
||||
|
||||
for (
|
||||
const Plant
|
||||
of Object.values(Plants)
|
||||
) {
|
||||
if (
|
||||
!IsPlantAvailableInShop(
|
||||
SaveData,
|
||||
Plant
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!DoesPlantMatchRecipeMatcher(
|
||||
Plant,
|
||||
Matcher
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Cost =
|
||||
GetPlantShopCost(
|
||||
SaveData,
|
||||
Plant.Id,
|
||||
Cache,
|
||||
Visiting
|
||||
);
|
||||
|
||||
if (Cost === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
CheapestCost === null ||
|
||||
Cost < CheapestCost
|
||||
) {
|
||||
CheapestCost = Cost;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return CheapestCost;
|
||||
}
|
||||
|
||||
|
||||
function DoesPlantMatchRecipeMatcher(
|
||||
Plant,
|
||||
Matcher
|
||||
) {
|
||||
const Tags =
|
||||
Plant.Tags ?? [];
|
||||
|
||||
if (
|
||||
Array.isArray(
|
||||
Matcher.Tags
|
||||
) &&
|
||||
!Matcher.Tags.every(
|
||||
Tag =>
|
||||
Tags.includes(Tag)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(
|
||||
Matcher.TagsAny
|
||||
) &&
|
||||
Matcher.TagsAny.length > 0 &&
|
||||
!Matcher.TagsAny.some(
|
||||
Tag =>
|
||||
Tags.includes(Tag)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(
|
||||
Matcher.TagsNot
|
||||
) &&
|
||||
Matcher.TagsNot.some(
|
||||
Tag =>
|
||||
Tags.includes(Tag)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function GetMutationTimeFee(
|
||||
Mutation
|
||||
) {
|
||||
const MutationTime = Number(
|
||||
Mutation.MutationTime ??
|
||||
Mutation.Cooldown ??
|
||||
0
|
||||
);
|
||||
|
||||
if (
|
||||
!Number.isFinite(
|
||||
MutationTime
|
||||
) ||
|
||||
MutationTime <= 0
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.ceil(
|
||||
MutationTime /
|
||||
3600000 *
|
||||
MutationTimeFeePerHour
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function GetPlantHarvestReward(
|
||||
SaveData,
|
||||
PlantId
|
||||
) {
|
||||
const Cost =
|
||||
GetPlantShopCost(
|
||||
SaveData,
|
||||
PlantId
|
||||
);
|
||||
|
||||
if (Cost === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.ceil(
|
||||
Cost * 1.5
|
||||
);
|
||||
}
|
||||
+398
-191
@@ -1,62 +1,34 @@
|
||||
let GameSave;
|
||||
let SelectedSeed = "Rose";
|
||||
let SelectedSeedId = null;
|
||||
|
||||
const GardenUpdateInterval = 1000;
|
||||
|
||||
|
||||
async function StartGame() {
|
||||
GameSave = await LoadGame();
|
||||
NormalizeSave();
|
||||
|
||||
EnsureSelectedSeed();
|
||||
RenderGame();
|
||||
|
||||
setInterval(
|
||||
RenderGarden,
|
||||
() => {
|
||||
RenderGarden();
|
||||
RenderNextHarvest();
|
||||
},
|
||||
GardenUpdateInterval
|
||||
);
|
||||
|
||||
await RenderUser();
|
||||
await RenderLeaderboard();
|
||||
}
|
||||
|
||||
|
||||
function NormalizeSave() {
|
||||
GameSave.Currency ??= {};
|
||||
GameSave.Currency.Dew ??= 0;
|
||||
|
||||
GameSave.Seeds ??= {};
|
||||
GameSave.Seeds.Unlocked ??= [
|
||||
"Rose"
|
||||
];
|
||||
|
||||
GameSave.Statistics ??= {};
|
||||
GameSave.Statistics.CurrencyEarned ??= {};
|
||||
|
||||
GameSave.Statistics.CurrencyEarned.Dew ??=
|
||||
GameSave.Currency.Dew;
|
||||
|
||||
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() {
|
||||
EnsureSelectedSeed();
|
||||
RenderCurrency();
|
||||
RenderSeeds();
|
||||
RenderGarden();
|
||||
RenderNextHarvest();
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +39,46 @@ function RenderCurrency() {
|
||||
);
|
||||
|
||||
DewAmount.textContent =
|
||||
GameSave.Currency.Dew;
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,14 +90,72 @@ function RenderSeeds() {
|
||||
|
||||
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 GameSave.Seeds.Unlocked
|
||||
of SeedIds
|
||||
) {
|
||||
const Plant =
|
||||
Plants[PlantId];
|
||||
GetPlantById(
|
||||
PlantId
|
||||
);
|
||||
|
||||
if (Plant === undefined) {
|
||||
if (Plant === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -101,11 +170,16 @@ function RenderSeeds() {
|
||||
Button.type = "button";
|
||||
|
||||
Button.textContent =
|
||||
Plant.Name;
|
||||
Plant.Name +
|
||||
" ×" +
|
||||
GetSeedCount(
|
||||
GameSave,
|
||||
PlantId
|
||||
);
|
||||
|
||||
if (
|
||||
PlantId ===
|
||||
SelectedSeed
|
||||
SelectedSeedId
|
||||
) {
|
||||
Button.setAttribute(
|
||||
"aria-current",
|
||||
@@ -116,7 +190,7 @@ function RenderSeeds() {
|
||||
Button.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
SelectedSeed =
|
||||
SelectedSeedId =
|
||||
PlantId;
|
||||
|
||||
RenderSeeds();
|
||||
@@ -180,10 +254,6 @@ function CreatePlotElement(
|
||||
Button.type = "button";
|
||||
|
||||
|
||||
/*
|
||||
* Empty plot.
|
||||
*/
|
||||
|
||||
if (Plot === null) {
|
||||
Button.classList.add(
|
||||
"GardenPlotEmpty"
|
||||
@@ -215,10 +285,6 @@ function CreatePlotElement(
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Plant image.
|
||||
*/
|
||||
|
||||
const ImagePath =
|
||||
GetPlantImage(
|
||||
Plot,
|
||||
@@ -243,13 +309,24 @@ function CreatePlotElement(
|
||||
Button.appendChild(
|
||||
Image
|
||||
);
|
||||
} else {
|
||||
const MissingImage =
|
||||
document.createElement(
|
||||
"span"
|
||||
);
|
||||
|
||||
MissingImage.className =
|
||||
"GardenPlantMissing";
|
||||
|
||||
MissingImage.textContent =
|
||||
Plant.Name;
|
||||
|
||||
Button.appendChild(
|
||||
MissingImage
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Mature plants can be harvested.
|
||||
*/
|
||||
|
||||
if (
|
||||
IsPlantMature(
|
||||
Plot,
|
||||
@@ -288,6 +365,7 @@ function CreatePlotElement(
|
||||
"% grown";
|
||||
}
|
||||
|
||||
|
||||
return Button;
|
||||
}
|
||||
|
||||
@@ -303,34 +381,63 @@ async function PlantSeed(
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureSelectedSeed();
|
||||
|
||||
if (SelectedSeedId === null) {
|
||||
SetGardenMessage(
|
||||
"You don't have any seeds to plant."
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const Plant =
|
||||
GetPlantById(
|
||||
SelectedSeedId
|
||||
);
|
||||
|
||||
const PlantKey =
|
||||
GetPlantKeyById(
|
||||
SelectedSeedId
|
||||
);
|
||||
|
||||
if (
|
||||
!GameSave.Seeds.Unlocked
|
||||
.includes(
|
||||
SelectedSeed
|
||||
)
|
||||
Plant === null ||
|
||||
PlantKey === null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
!TakeSeed(
|
||||
GameSave,
|
||||
SelectedSeedId
|
||||
)
|
||||
) {
|
||||
EnsureSelectedSeed();
|
||||
RenderSeeds();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
GameSave.Garden.Plots[
|
||||
PlotIndex
|
||||
] = {
|
||||
Plant: SelectedSeed,
|
||||
|
||||
AddedTags: [],
|
||||
|
||||
VisualVariant: null,
|
||||
|
||||
Plant: PlantKey,
|
||||
PlantedAt: Date.now()
|
||||
};
|
||||
|
||||
|
||||
SetGardenMessage(
|
||||
"Planted " +
|
||||
Plants[SelectedSeed].Name +
|
||||
Plant.Name +
|
||||
"."
|
||||
);
|
||||
|
||||
RenderGarden();
|
||||
RenderGame();
|
||||
|
||||
await SaveGame(
|
||||
GameSave
|
||||
@@ -354,6 +461,7 @@ async function HarvestPlant(
|
||||
Plants[Plot.Plant];
|
||||
|
||||
if (
|
||||
Plant === undefined ||
|
||||
!IsPlantMature(
|
||||
Plot,
|
||||
Plant
|
||||
@@ -362,22 +470,40 @@ async function HarvestPlant(
|
||||
return;
|
||||
}
|
||||
|
||||
GiveReward(
|
||||
Plant.Reward
|
||||
);
|
||||
|
||||
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 " +
|
||||
Plant.Reward.Amount +
|
||||
" " +
|
||||
Plant.Reward.Currency +
|
||||
"."
|
||||
RewardAmount +
|
||||
" Dew."
|
||||
);
|
||||
|
||||
RenderGame();
|
||||
@@ -437,63 +563,17 @@ function GetPlantGrowthProgress(
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function GetPlantTags(
|
||||
Plot,
|
||||
Plant
|
||||
) {
|
||||
return [
|
||||
...Plant.Tags,
|
||||
...(Plot.AddedTags ?? [])
|
||||
...(Plant.Tags ?? [])
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
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 IsPlantMature(
|
||||
Plot,
|
||||
Plant
|
||||
@@ -511,74 +591,9 @@ function GetPlantImages(
|
||||
Plot,
|
||||
Plant
|
||||
) {
|
||||
const Variants =
|
||||
PlantImages[
|
||||
Plot.Plant
|
||||
];
|
||||
|
||||
if (Variants === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const Variant =
|
||||
GetPlantVariant(
|
||||
Plot,
|
||||
Plant
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Preferred variant.
|
||||
*/
|
||||
|
||||
if (
|
||||
Array.isArray(
|
||||
Variants[Variant]
|
||||
) &&
|
||||
Variants[Variant].length > 0
|
||||
) {
|
||||
return Variants[
|
||||
Variant
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Default visual fallback.
|
||||
*/
|
||||
|
||||
if (
|
||||
Array.isArray(
|
||||
Variants.Default
|
||||
) &&
|
||||
Variants.Default.length > 0
|
||||
) {
|
||||
return Variants.Default;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Absolute fallback:
|
||||
* use the first variant that has images.
|
||||
*/
|
||||
|
||||
for (
|
||||
const Images
|
||||
of Object.values(
|
||||
Variants
|
||||
)
|
||||
) {
|
||||
if (
|
||||
Array.isArray(Images) &&
|
||||
Images.length > 0
|
||||
) {
|
||||
return Images;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return [];
|
||||
return PlantImages[
|
||||
Plot.Plant
|
||||
] ?? [];
|
||||
}
|
||||
|
||||
|
||||
@@ -642,6 +657,198 @@ function SetGardenMessage(
|
||||
}
|
||||
|
||||
|
||||
function GetNextHarvestTime() {
|
||||
let NextHarvestTime = 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 (
|
||||
HarvestTime <=
|
||||
Date.now()
|
||||
) {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
NextHarvestTime === null ||
|
||||
HarvestTime <
|
||||
NextHarvestTime
|
||||
) {
|
||||
NextHarvestTime =
|
||||
HarvestTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return NextHarvestTime;
|
||||
}
|
||||
|
||||
|
||||
function RenderNextHarvest() {
|
||||
const Element =
|
||||
document.getElementById(
|
||||
"NextHarvest"
|
||||
);
|
||||
|
||||
if (Element === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const HarvestTime =
|
||||
GetNextHarvestTime();
|
||||
|
||||
|
||||
if (HarvestTime === null) {
|
||||
Element.textContent =
|
||||
"Nothing planted";
|
||||
|
||||
Element.removeAttribute(
|
||||
"title"
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const Remaining =
|
||||
HarvestTime -
|
||||
Date.now();
|
||||
|
||||
|
||||
if (Remaining <= 0) {
|
||||
Element.textContent =
|
||||
"Ready now!";
|
||||
|
||||
Element.removeAttribute(
|
||||
"title"
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Element.textContent =
|
||||
FormatRemainingTime(
|
||||
Remaining
|
||||
);
|
||||
|
||||
|
||||
const HarvestDate =
|
||||
new Date(
|
||||
HarvestTime
|
||||
);
|
||||
|
||||
|
||||
Element.title =
|
||||
"Ready at " +
|
||||
HarvestDate.toLocaleTimeString(
|
||||
[],
|
||||
{
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
StartGame
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
const MutationSets = {
|
||||
DarkRose: {
|
||||
Name: "Dark Rose",
|
||||
RoseDarkening: {
|
||||
Id: 1,
|
||||
Name: "Rose darkening",
|
||||
|
||||
Description:
|
||||
"A mutation that seems to corrupt roses.",
|
||||
|
||||
Chance: 0.1,
|
||||
Cooldown: 120000,
|
||||
|
||||
Relations: {
|
||||
PlantsUsed: [
|
||||
1
|
||||
],
|
||||
|
||||
PlantsCreated: [
|
||||
2
|
||||
]
|
||||
},
|
||||
|
||||
Pattern: [
|
||||
[
|
||||
{
|
||||
@@ -35,6 +49,60 @@ const MutationSets = {
|
||||
]
|
||||
],
|
||||
|
||||
Failure: "Keep"
|
||||
},
|
||||
|
||||
RoseDyePurple: {
|
||||
Id: 2,
|
||||
Name: "Rose dying purple",
|
||||
|
||||
Description:
|
||||
"A colourful mutation that makes a purple rose.",
|
||||
|
||||
Chance: 1.0,
|
||||
Cooldown: 100000,
|
||||
|
||||
Relations: {
|
||||
PlantsUsed: [
|
||||
1,
|
||||
3
|
||||
],
|
||||
|
||||
PlantsCreated: [
|
||||
4
|
||||
]
|
||||
},
|
||||
|
||||
Pattern: [
|
||||
[
|
||||
{
|
||||
Tags: [
|
||||
"Rose",
|
||||
"Red"
|
||||
],
|
||||
Capture: "ParentA"
|
||||
},
|
||||
|
||||
"Empty",
|
||||
|
||||
{
|
||||
Tags: [
|
||||
"Rose",
|
||||
"Blue"
|
||||
],
|
||||
Capture: "ParentB"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
Success: [
|
||||
[
|
||||
"Empty",
|
||||
"PurpleRose",
|
||||
"Empty"
|
||||
]
|
||||
],
|
||||
|
||||
Failure: "Keep"
|
||||
}
|
||||
};
|
||||
+71
-22
@@ -1,6 +1,10 @@
|
||||
const Plants = {
|
||||
Rose: {
|
||||
Name: "Rose",
|
||||
RedRose: {
|
||||
Id: 1,
|
||||
Name: "Red Rose",
|
||||
|
||||
Description:
|
||||
"A common red rose.",
|
||||
|
||||
Tags: [
|
||||
"Rose",
|
||||
@@ -8,29 +12,74 @@ const Plants = {
|
||||
"Red"
|
||||
],
|
||||
|
||||
GrowthTime: 60000,
|
||||
|
||||
Variants: [
|
||||
{
|
||||
Name: "Red",
|
||||
Tags: [
|
||||
"Red"
|
||||
],
|
||||
Priority: 10
|
||||
},
|
||||
|
||||
{
|
||||
Name: "Default",
|
||||
Tags: [],
|
||||
Priority: 0
|
||||
}
|
||||
],
|
||||
GrowthTime: 3600000,
|
||||
|
||||
Effects: [],
|
||||
|
||||
Reward: {
|
||||
Currency: "Dew",
|
||||
Amount: 5
|
||||
Shop: {
|
||||
StartingPlant: true,
|
||||
BaseCost: 10
|
||||
}
|
||||
},
|
||||
|
||||
DarkRose: {
|
||||
Id: 2,
|
||||
Name: "Dark Rose",
|
||||
|
||||
Description:
|
||||
"A jet-black rose.",
|
||||
|
||||
Tags: [
|
||||
"Rose",
|
||||
"Flower",
|
||||
"Black"
|
||||
],
|
||||
|
||||
GrowthTime: 4000000,
|
||||
|
||||
Effects: [],
|
||||
|
||||
Shop: {
|
||||
StartingPlant: false
|
||||
}
|
||||
},
|
||||
|
||||
BlueRose: {
|
||||
Id: 3,
|
||||
Name: "Blue Rose",
|
||||
|
||||
Tags: [
|
||||
"Rose",
|
||||
"Flower",
|
||||
"Blue"
|
||||
],
|
||||
|
||||
GrowthTime: 3600000,
|
||||
|
||||
Effects: [],
|
||||
|
||||
Shop: {
|
||||
StartingPlant: true,
|
||||
BaseCost: 15
|
||||
}
|
||||
},
|
||||
|
||||
PurpleRose: {
|
||||
Id: 4,
|
||||
Name: "Purple Rose",
|
||||
|
||||
Tags: [
|
||||
"Rose",
|
||||
"Flower",
|
||||
"Purple"
|
||||
],
|
||||
|
||||
GrowthTime: 3600000,
|
||||
|
||||
Effects: [],
|
||||
|
||||
Shop: {
|
||||
StartingPlant: false
|
||||
}
|
||||
}
|
||||
};
|
||||
+500
-12
@@ -5,8 +5,45 @@ const ApiUrl = "https://api.srhtnin.garden";
|
||||
|
||||
|
||||
function CreateNewSave() {
|
||||
const StartingSeeds = {};
|
||||
const StartingPlants = [];
|
||||
|
||||
if (
|
||||
typeof Plants !==
|
||||
"undefined"
|
||||
) {
|
||||
for (
|
||||
const Plant
|
||||
of Object.values(Plants)
|
||||
) {
|
||||
if (
|
||||
Plant.Shop
|
||||
?.StartingPlant !==
|
||||
true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
StartingSeeds[
|
||||
String(Plant.Id)
|
||||
] = 1;
|
||||
|
||||
StartingPlants.push(
|
||||
Plant.Id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
StartingPlants.length === 0
|
||||
) {
|
||||
StartingSeeds["1"] = 1;
|
||||
StartingPlants.push(1);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
Version: 1,
|
||||
Version: 2,
|
||||
Revision: 0,
|
||||
LastSavedAt: Date.now(),
|
||||
|
||||
@@ -17,13 +54,17 @@ function CreateNewSave() {
|
||||
Statistics: {
|
||||
CurrencyEarned: {
|
||||
Dew: 0
|
||||
}
|
||||
},
|
||||
|
||||
CurrencySpent: {
|
||||
Dew: 0
|
||||
},
|
||||
|
||||
SeedsPurchased: 0
|
||||
},
|
||||
|
||||
Seeds: {
|
||||
Unlocked: [
|
||||
"Rose"
|
||||
]
|
||||
Inventory: {
|
||||
Seeds: StartingSeeds
|
||||
},
|
||||
|
||||
Upgrades: {
|
||||
@@ -37,10 +78,7 @@ function CreateNewSave() {
|
||||
},
|
||||
|
||||
Discoveries: {
|
||||
Plants: [
|
||||
"Rose"
|
||||
],
|
||||
|
||||
Plants: StartingPlants,
|
||||
Mutations: []
|
||||
},
|
||||
|
||||
@@ -49,6 +87,445 @@ function CreateNewSave() {
|
||||
}
|
||||
|
||||
|
||||
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.Garden ??= {
|
||||
Width: 3,
|
||||
Height: 3,
|
||||
Plots: []
|
||||
};
|
||||
|
||||
SaveData.Garden.Width = Math.max(
|
||||
1,
|
||||
Number(
|
||||
SaveData.Garden.Width ?? 3
|
||||
)
|
||||
);
|
||||
|
||||
SaveData.Garden.Height = Math.max(
|
||||
1,
|
||||
Number(
|
||||
SaveData.Garden.Height ?? 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() {
|
||||
if (
|
||||
typeof Plants ===
|
||||
"undefined"
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.values(Plants)
|
||||
.filter(
|
||||
Plant =>
|
||||
Plant.Shop
|
||||
?.StartingPlant ===
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
@@ -96,7 +573,9 @@ function ReadLocalSave() {
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(RawSave);
|
||||
return NormalizeSaveData(
|
||||
JSON.parse(RawSave)
|
||||
);
|
||||
} catch (Error) {
|
||||
console.error(
|
||||
"Couldn't read local save:",
|
||||
@@ -185,10 +664,17 @@ async function ReadRemoteSave(
|
||||
return null;
|
||||
}
|
||||
|
||||
return Result.SaveData;
|
||||
return NormalizeSaveData(
|
||||
Result.SaveData
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
async function SaveGame(SaveData) {
|
||||
NormalizeSaveData(
|
||||
SaveData
|
||||
);
|
||||
|
||||
SaveData.Revision++;
|
||||
SaveData.LastSavedAt = Date.now();
|
||||
|
||||
@@ -304,5 +790,7 @@ async function LoadGame() {
|
||||
return RemoteSave;
|
||||
}
|
||||
|
||||
WriteLocalSave(LocalSave);
|
||||
|
||||
return LocalSave;
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
let ShopSave;
|
||||
let ShopPurchasePending = false;
|
||||
|
||||
|
||||
async function StartShop() {
|
||||
ShopSave = await LoadGame();
|
||||
|
||||
RenderShop();
|
||||
}
|
||||
|
||||
|
||||
function RenderShop() {
|
||||
RenderShopCurrency();
|
||||
RenderShopSeeds();
|
||||
}
|
||||
|
||||
|
||||
function RenderShopCurrency() {
|
||||
const DewAmount =
|
||||
document.getElementById(
|
||||
"ShopDewAmount"
|
||||
);
|
||||
|
||||
if (DewAmount === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DewAmount.textContent =
|
||||
ShopSave.Currency.Dew
|
||||
.toLocaleString();
|
||||
}
|
||||
|
||||
|
||||
function RenderShopSeeds() {
|
||||
const SeedList =
|
||||
document.getElementById(
|
||||
"ShopSeedList"
|
||||
);
|
||||
|
||||
SeedList.replaceChildren();
|
||||
|
||||
|
||||
const AvailablePlants =
|
||||
Object.values(Plants)
|
||||
.filter(
|
||||
Plant =>
|
||||
IsPlantAvailableInShop(
|
||||
ShopSave,
|
||||
Plant
|
||||
)
|
||||
)
|
||||
.sort(
|
||||
(A, B) =>
|
||||
A.Id - B.Id
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
AvailablePlants.length === 0
|
||||
) {
|
||||
const EmptyMessage =
|
||||
document.createElement(
|
||||
"p"
|
||||
);
|
||||
|
||||
EmptyMessage.className =
|
||||
"ShopEmpty";
|
||||
|
||||
EmptyMessage.textContent =
|
||||
"No seeds are available yet.";
|
||||
|
||||
SeedList.appendChild(
|
||||
EmptyMessage
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (
|
||||
const Plant
|
||||
of AvailablePlants
|
||||
) {
|
||||
SeedList.appendChild(
|
||||
CreateShopSeedCard(
|
||||
Plant
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function CreateShopSeedCard(
|
||||
Plant
|
||||
) {
|
||||
const Card =
|
||||
document.createElement(
|
||||
"article"
|
||||
);
|
||||
|
||||
Card.className =
|
||||
"ShopItem";
|
||||
|
||||
|
||||
const Header =
|
||||
document.createElement(
|
||||
"div"
|
||||
);
|
||||
|
||||
Header.className =
|
||||
"ShopItemHeader";
|
||||
|
||||
|
||||
const Number =
|
||||
document.createElement(
|
||||
"span"
|
||||
);
|
||||
|
||||
Number.className =
|
||||
"ShopItemNumber";
|
||||
|
||||
Number.textContent =
|
||||
"No. " +
|
||||
String(Plant.Id).padStart(
|
||||
3,
|
||||
"0"
|
||||
);
|
||||
|
||||
|
||||
const Name =
|
||||
document.createElement(
|
||||
"h3"
|
||||
);
|
||||
|
||||
Name.textContent =
|
||||
Plant.Name;
|
||||
|
||||
|
||||
Header.append(
|
||||
Number,
|
||||
Name
|
||||
);
|
||||
|
||||
|
||||
const Body =
|
||||
document.createElement(
|
||||
"div"
|
||||
);
|
||||
|
||||
Body.className =
|
||||
"ShopItemBody";
|
||||
|
||||
|
||||
const Visual =
|
||||
CreateShopPlantVisual(
|
||||
Plant
|
||||
);
|
||||
|
||||
|
||||
const Details =
|
||||
document.createElement(
|
||||
"div"
|
||||
);
|
||||
|
||||
Details.className =
|
||||
"ShopItemDetails";
|
||||
|
||||
|
||||
if (
|
||||
typeof Plant.Description ===
|
||||
"string" &&
|
||||
Plant.Description.length > 0
|
||||
) {
|
||||
const Description =
|
||||
document.createElement(
|
||||
"p"
|
||||
);
|
||||
|
||||
Description.textContent =
|
||||
Plant.Description;
|
||||
|
||||
Details.appendChild(
|
||||
Description
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const Cost =
|
||||
GetPlantShopCost(
|
||||
ShopSave,
|
||||
Plant.Id
|
||||
);
|
||||
|
||||
const Reward =
|
||||
GetPlantHarvestReward(
|
||||
ShopSave,
|
||||
Plant.Id
|
||||
);
|
||||
|
||||
|
||||
Details.appendChild(
|
||||
CreateShopStat(
|
||||
"Seed cost",
|
||||
Cost === null
|
||||
? "Unavailable"
|
||||
: Cost.toLocaleString() +
|
||||
" Dew"
|
||||
)
|
||||
);
|
||||
|
||||
Details.appendChild(
|
||||
CreateShopStat(
|
||||
"Harvest reward",
|
||||
Reward === null
|
||||
? "Unavailable"
|
||||
: Reward.toLocaleString() +
|
||||
" Dew"
|
||||
)
|
||||
);
|
||||
|
||||
Details.appendChild(
|
||||
CreateShopStat(
|
||||
"Inventory",
|
||||
GetSeedCount(
|
||||
ShopSave,
|
||||
Plant.Id
|
||||
).toLocaleString()
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Body.append(
|
||||
Visual,
|
||||
Details
|
||||
);
|
||||
|
||||
|
||||
const BuyButton =
|
||||
document.createElement(
|
||||
"button"
|
||||
);
|
||||
|
||||
BuyButton.className =
|
||||
"ShopBuyButton";
|
||||
|
||||
BuyButton.type = "button";
|
||||
|
||||
BuyButton.textContent =
|
||||
Cost === null
|
||||
? "Unavailable"
|
||||
: "Buy seed - " +
|
||||
Cost.toLocaleString() +
|
||||
" Dew";
|
||||
|
||||
BuyButton.disabled =
|
||||
Cost === null ||
|
||||
ShopSave.Currency.Dew <
|
||||
Cost ||
|
||||
ShopPurchasePending;
|
||||
|
||||
BuyButton.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
BuyPlantSeed(
|
||||
Plant.Id
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Card.append(
|
||||
Header,
|
||||
Body,
|
||||
BuyButton
|
||||
);
|
||||
|
||||
|
||||
return Card;
|
||||
}
|
||||
|
||||
|
||||
function CreateShopPlantVisual(
|
||||
Plant
|
||||
) {
|
||||
const Visual =
|
||||
document.createElement(
|
||||
"div"
|
||||
);
|
||||
|
||||
Visual.className =
|
||||
"ShopPlantVisual";
|
||||
|
||||
|
||||
const PlantKey =
|
||||
GetPlantKeyById(
|
||||
Plant.Id
|
||||
);
|
||||
|
||||
const Images =
|
||||
PlantKey === null
|
||||
? []
|
||||
: PlantImages[
|
||||
PlantKey
|
||||
] ?? [];
|
||||
|
||||
|
||||
if (Images.length === 0) {
|
||||
const Missing =
|
||||
document.createElement(
|
||||
"span"
|
||||
);
|
||||
|
||||
Missing.className =
|
||||
"ShopPlantMissing";
|
||||
|
||||
Missing.textContent =
|
||||
"No image";
|
||||
|
||||
Visual.appendChild(
|
||||
Missing
|
||||
);
|
||||
|
||||
return Visual;
|
||||
}
|
||||
|
||||
|
||||
const Image =
|
||||
document.createElement(
|
||||
"img"
|
||||
);
|
||||
|
||||
Image.className =
|
||||
"ShopPlantImage";
|
||||
|
||||
Image.src =
|
||||
Images[
|
||||
Images.length - 1
|
||||
];
|
||||
|
||||
Image.alt =
|
||||
Plant.Name;
|
||||
|
||||
Visual.appendChild(
|
||||
Image
|
||||
);
|
||||
|
||||
|
||||
return Visual;
|
||||
}
|
||||
|
||||
|
||||
function CreateShopStat(
|
||||
Name,
|
||||
Value
|
||||
) {
|
||||
const Row =
|
||||
document.createElement(
|
||||
"p"
|
||||
);
|
||||
|
||||
Row.className =
|
||||
"ShopStat";
|
||||
|
||||
|
||||
const Label =
|
||||
document.createElement(
|
||||
"span"
|
||||
);
|
||||
|
||||
Label.className =
|
||||
"ShopStatName";
|
||||
|
||||
Label.textContent =
|
||||
Name + ":";
|
||||
|
||||
|
||||
const Amount =
|
||||
document.createElement(
|
||||
"strong"
|
||||
);
|
||||
|
||||
Amount.textContent =
|
||||
Value;
|
||||
|
||||
|
||||
Row.append(
|
||||
Label,
|
||||
Amount
|
||||
);
|
||||
|
||||
|
||||
return Row;
|
||||
}
|
||||
|
||||
|
||||
async function BuyPlantSeed(
|
||||
PlantId
|
||||
) {
|
||||
if (ShopPurchasePending) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const Plant =
|
||||
GetPlantById(
|
||||
PlantId
|
||||
);
|
||||
|
||||
if (
|
||||
Plant === null ||
|
||||
!IsPlantAvailableInShop(
|
||||
ShopSave,
|
||||
Plant
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const Cost =
|
||||
GetPlantShopCost(
|
||||
ShopSave,
|
||||
PlantId
|
||||
);
|
||||
|
||||
if (Cost === null) {
|
||||
SetShopMessage(
|
||||
"That seed doesn't have a purchasable recipe yet."
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
ShopSave.Currency.Dew <
|
||||
Cost
|
||||
) {
|
||||
SetShopMessage(
|
||||
"You don't have enough Dew for " +
|
||||
Plant.Name +
|
||||
"."
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ShopPurchasePending = true;
|
||||
|
||||
ShopSave.Currency.Dew -=
|
||||
Cost;
|
||||
|
||||
ShopSave.Statistics.CurrencySpent.Dew +=
|
||||
Cost;
|
||||
|
||||
ShopSave.Statistics.SeedsPurchased++;
|
||||
|
||||
AddSeed(
|
||||
ShopSave,
|
||||
PlantId
|
||||
);
|
||||
|
||||
|
||||
SetShopMessage(
|
||||
"Bought a " +
|
||||
Plant.Name +
|
||||
" seed for " +
|
||||
Cost +
|
||||
" Dew."
|
||||
);
|
||||
|
||||
RenderShop();
|
||||
|
||||
|
||||
await SaveGame(
|
||||
ShopSave
|
||||
);
|
||||
|
||||
|
||||
ShopPurchasePending = false;
|
||||
RenderShop();
|
||||
}
|
||||
|
||||
|
||||
function SetShopMessage(
|
||||
Message
|
||||
) {
|
||||
const Element =
|
||||
document.getElementById(
|
||||
"ShopMessage"
|
||||
);
|
||||
|
||||
if (Element !== null) {
|
||||
Element.textContent =
|
||||
Message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
StartShop
|
||||
);
|
||||
Reference in new issue
Block a user