Added more stuff to profiles, including descriptions and profile

pictures. Very cool.
This commit is contained in:
sarah committed 2026-09-07 17:35:38 +02:00
1 parent 7f9284c5e2
commit d1721517cc
7 files changed
+1557 -247

No files matched your search

+1
View File
@@ -72,6 +72,7 @@ $PageSection = "Plants";
<script src="/Scripts/Garden/Save.js"></script> <script src="/Scripts/Garden/Save.js"></script>
<script src="/Scripts/Garden/Content.js"></script> <script src="/Scripts/Garden/Content.js"></script>
<script src="/Scripts/Garden/Economy.js"></script> <script src="/Scripts/Garden/Economy.js"></script>
<script src="/Scripts/Garden/PlantCatalogue.js"></script>
<script src="/Scripts/Garden/PlantEncyclopedia.js"></script> <script src="/Scripts/Garden/PlantEncyclopedia.js"></script>
</body> </body>
</html> </html>
+169
View File
@@ -99,6 +99,172 @@ $PageTitle = "Profile";
</form> </form>
</section> </section>
<section
id="GardenerProfile"
class="Panel ProfileSection"
>
<h2 class="PanelHeader PanelHeaderInset">Gardener profile</h2>
<form id="ProfileDescriptionForm">
<label
for="ProfileDescriptionInput"
>
Description
</label>
<textarea
id="ProfileDescriptionInput"
rows="4"
maxlength="500"
placeholder="Write a short description..."
></textarea>
<div class="ProfileButtonRow">
<button type="submit">
Save description
</button>
</div>
<p
id="ProfileDescriptionMessage"
class="ProfileMessage"
></p>
</form>
<h3>Profile picture</h3>
<p>
Choose any plant you have discovered.
The selected growth stage is used anywhere
your Gardener profile picture is shown.
</p>
<div class="ProfilePictureSelectionRow">
<div>
<span class="ProfilePictureSelectionLabel">
Selected
</span>
<strong id="ProfilePictureSelectedName">
None
</strong>
</div>
<label
class="ProfilePictureGrowthStageLabel"
for="ProfilePictureGrowthStageSelect"
>
Growth stage
<select
id="ProfilePictureGrowthStageSelect"
disabled
>
<option value="">None</option>
</select>
</label>
<button
id="SaveProfilePictureButton"
type="button"
disabled
>
Save
</button>
</div>
<div
class="CatalogueControls"
aria-label="Profile picture plant search and sorting"
>
<input
id="ProfilePictureSearchInput"
type="search"
placeholder="Search plants..."
aria-label="Search profile picture plants"
autocomplete="off"
>
<select
id="ProfilePictureSortSelect"
aria-label="Sort profile picture plants"
>
<option value="IdAsc">ID</option>
<option value="NameAsc">Name A-Z</option>
<option value="GrowthAsc">Growth short-long</option>
<option value="GrowthDesc">Growth long-short</option>
<option value="CostAsc">Cost low-high</option>
<option value="CostDesc">Cost high-low</option>
<option value="RewardAsc">Reward low-high</option>
<option value="RewardDesc">Reward high-low</option>
<option value="DphDesc">DPH high-low</option>
<option value="DphAsc">DPH low-high</option>
</select>
</div>
<div
id="ProfilePictureGrid"
class="ProfilePictureGrid"
></div>
<p
id="ProfilePictureCatalogueMessage"
class="ProfileMessage"
aria-live="polite"
>
Loading discovered plants...
</p>
<p
id="ProfilePictureMessage"
class="ProfileMessage"
aria-live="polite"
></p>
<h3>Privacy</h3>
<p>
Choose which parts of your Gardener profile
other players will be able to use or view.
</p>
<div class="ProfileButtonRow">
<button
id="ToggleGardenPreviewsButton"
type="button"
>
Garden previews: On
</button>
<button
id="ToggleCommentsButton"
type="button"
>
Comments: On
</button>
<button
id="ToggleDewDonationsButton"
type="button"
>
Dew donations: On
</button>
<button
id="ToggleProfileStatsButton"
type="button"
>
Statistics: On
</button>
</div>
<p
id="ProfilePrivacyMessage"
class="ProfileMessage"
aria-live="polite"
></p>
</section>
<section <section
id="GardenDisplay" id="GardenDisplay"
class="Panel ProfileSection" class="Panel ProfileSection"
@@ -506,12 +672,15 @@ $PageTitle = "Profile";
</main> </main>
<script src="/Scripts/Garden/PlantImages.js"></script>
<script src="/Scripts/Garden/Plants.js"></script> <script src="/Scripts/Garden/Plants.js"></script>
<script src="/Scripts/Garden/Mutations.js"></script> <script src="/Scripts/Garden/Mutations.js"></script>
<script src="/Scripts/Garden/Save.js"></script> <script src="/Scripts/Garden/Save.js"></script>
<script src="/Scripts/Garden/Content.js"></script> <script src="/Scripts/Garden/Content.js"></script>
<script src="/Scripts/Garden/Upgrades.js"></script> <script src="/Scripts/Garden/Upgrades.js"></script>
<script src="/Scripts/Garden/Users.js"></script> <script src="/Scripts/Garden/Users.js"></script>
<script src="/Scripts/Garden/Economy.js"></script>
<script src="/Scripts/Garden/PlantCatalogue.js"></script>
<script src="/Scripts/Garden/Profile.js"></script> <script src="/Scripts/Garden/Profile.js"></script>
<script src="/Scripts/Garden/Account.js"></script> <script src="/Scripts/Garden/Account.js"></script>
</body> </body>
+233
View File
@@ -0,0 +1,233 @@
function GetPlantRelatedMutations(
PlantId,
RelationName
) {
return Object.values(
MutationSets
)
.filter(
Mutation =>
Mutation.Relations?.[
RelationName
]?.includes(
Number(PlantId)
)
)
.sort(
(A, B) =>
A.Id - B.Id
);
}
function DoesPlantCatalogueMatchSearch(
Plant,
SearchQuery,
SaveData
) {
if (SearchQuery.length === 0) {
return true;
}
const SearchParts = [
Plant.Id,
Plant.Name,
Plant.Description,
...(Array.isArray(Plant.Tags)
? Plant.Tags
: [])
];
for (
const Mutation
of GetPlantRelatedMutations(
Plant.Id,
"PlantsCreated"
).concat(
GetPlantRelatedMutations(
Plant.Id,
"PlantsUsed"
)
)
) {
if (
HasDiscoveredMutation(
SaveData,
Mutation.Id
)
) {
SearchParts.push(
Mutation.Name
);
}
}
return SearchParts
.filter(
Value =>
Value !== null &&
Value !== undefined
)
.join(" ")
.toLocaleLowerCase()
.includes(
SearchQuery
);
}
function ComparePlantCataloguePlants(
A,
B,
SortMode,
SaveData
) {
switch (SortMode) {
case "NameAsc":
return String(A.Name ?? "")
.localeCompare(
String(B.Name ?? ""),
undefined,
{sensitivity: "base"}
) || A.Id - B.Id;
case "GrowthAsc":
return ComparePlantCatalogueNumbers(
A.GrowthTime,
B.GrowthTime,
1
) || A.Id - B.Id;
case "GrowthDesc":
return ComparePlantCatalogueNumbers(
A.GrowthTime,
B.GrowthTime,
-1
) || A.Id - B.Id;
case "CostAsc":
return ComparePlantCatalogueNumbers(
GetPlantShopCost(
SaveData,
A.Id
),
GetPlantShopCost(
SaveData,
B.Id
),
1
) || A.Id - B.Id;
case "CostDesc":
return ComparePlantCatalogueNumbers(
GetPlantShopCost(
SaveData,
A.Id
),
GetPlantShopCost(
SaveData,
B.Id
),
-1
) || A.Id - B.Id;
case "RewardAsc":
return ComparePlantCatalogueNumbers(
GetPlantHarvestReward(
SaveData,
A.Id
),
GetPlantHarvestReward(
SaveData,
B.Id
),
1
) || A.Id - B.Id;
case "RewardDesc":
return ComparePlantCatalogueNumbers(
GetPlantHarvestReward(
SaveData,
A.Id
),
GetPlantHarvestReward(
SaveData,
B.Id
),
-1
) || A.Id - B.Id;
case "DphAsc":
return ComparePlantCatalogueNumbers(
GetPlantDewPerHour(
SaveData,
A.Id
),
GetPlantDewPerHour(
SaveData,
B.Id
),
1
) || A.Id - B.Id;
case "DphDesc":
return ComparePlantCatalogueNumbers(
GetPlantDewPerHour(
SaveData,
A.Id
),
GetPlantDewPerHour(
SaveData,
B.Id
),
-1
) || A.Id - B.Id;
case "IdAsc":
default:
return A.Id - B.Id;
}
}
function ComparePlantCatalogueNumbers(
A,
B,
Direction
) {
const MissingA =
A === null ||
A === undefined ||
A === "";
const MissingB =
B === null ||
B === undefined ||
B === "";
const NumberA = Number(A);
const NumberB = Number(B);
const ValidA =
!MissingA &&
Number.isFinite(NumberA);
const ValidB =
!MissingB &&
Number.isFinite(NumberB);
if (!ValidA && !ValidB) {
return 0;
}
if (!ValidA) {
return 1;
}
if (!ValidB) {
return -1;
}
return (NumberA - NumberB) *
Direction;
}
+8 -215
View File
@@ -166,53 +166,10 @@ function DoesPlantEncyclopediaMatchSearch(
Plant, Plant,
SearchQuery SearchQuery
) { ) {
if (SearchQuery.length === 0) { return DoesPlantCatalogueMatchSearch(
return true; Plant,
} SearchQuery,
PlantEncyclopediaSave
const SearchParts = [
Plant.Id,
Plant.Name,
Plant.Description,
...(Array.isArray(Plant.Tags)
? Plant.Tags
: [])
];
for (
const Mutation
of GetPlantRelatedMutations(
Plant.Id,
"PlantsCreated"
).concat(
GetPlantRelatedMutations(
Plant.Id,
"PlantsUsed"
)
)
) {
if (
HasDiscoveredMutation(
PlantEncyclopediaSave,
Mutation.Id
)
) {
SearchParts.push(
Mutation.Name
);
}
}
return SearchParts
.filter(
Value =>
Value !== null &&
Value !== undefined
)
.join(" ")
.toLocaleLowerCase()
.includes(
SearchQuery
); );
} }
@@ -221,154 +178,12 @@ function ComparePlantEncyclopediaPlants(
A, A,
B B
) { ) {
switch (PlantEncyclopediaSortMode) { return ComparePlantCataloguePlants(
case "NameAsc":
return String(A.Name ?? "")
.localeCompare(
String(B.Name ?? ""),
undefined,
{sensitivity: "base"}
) || A.Id - B.Id;
case "GrowthAsc":
return ComparePlantEncyclopediaNumbers(
A.GrowthTime,
B.GrowthTime,
1
) || A.Id - B.Id;
case "GrowthDesc":
return ComparePlantEncyclopediaNumbers(
A.GrowthTime,
B.GrowthTime,
-1
) || A.Id - B.Id;
case "CostAsc":
return ComparePlantEncyclopediaNumbers(
GetPlantShopCost(
PlantEncyclopediaSave,
A.Id
),
GetPlantShopCost(
PlantEncyclopediaSave,
B.Id
),
1
) || A.Id - B.Id;
case "CostDesc":
return ComparePlantEncyclopediaNumbers(
GetPlantShopCost(
PlantEncyclopediaSave,
A.Id
),
GetPlantShopCost(
PlantEncyclopediaSave,
B.Id
),
-1
) || A.Id - B.Id;
case "RewardAsc":
return ComparePlantEncyclopediaNumbers(
GetPlantHarvestReward(
PlantEncyclopediaSave,
A.Id
),
GetPlantHarvestReward(
PlantEncyclopediaSave,
B.Id
),
1
) || A.Id - B.Id;
case "RewardDesc":
return ComparePlantEncyclopediaNumbers(
GetPlantHarvestReward(
PlantEncyclopediaSave,
A.Id
),
GetPlantHarvestReward(
PlantEncyclopediaSave,
B.Id
),
-1
) || A.Id - B.Id;
case "DphAsc":
return ComparePlantEncyclopediaNumbers(
GetPlantDewPerHour(
PlantEncyclopediaSave,
A.Id
),
GetPlantDewPerHour(
PlantEncyclopediaSave,
B.Id
),
1
) || A.Id - B.Id;
case "DphDesc":
return ComparePlantEncyclopediaNumbers(
GetPlantDewPerHour(
PlantEncyclopediaSave,
A.Id
),
GetPlantDewPerHour(
PlantEncyclopediaSave,
B.Id
),
-1
) || A.Id - B.Id;
case "IdAsc":
default:
return A.Id - B.Id;
}
}
function ComparePlantEncyclopediaNumbers(
A, A,
B, B,
Direction PlantEncyclopediaSortMode,
) { PlantEncyclopediaSave
const MissingA = );
A === null ||
A === undefined ||
A === "";
const MissingB =
B === null ||
B === undefined ||
B === "";
const NumberA = Number(A);
const NumberB = Number(B);
const ValidA =
!MissingA &&
Number.isFinite(NumberA);
const ValidB =
!MissingB &&
Number.isFinite(NumberB);
if (!ValidA && !ValidB) {
return 0;
}
if (!ValidA) {
return 1;
}
if (!ValidB) {
return -1;
}
return (NumberA - NumberB) *
Direction;
} }
@@ -843,28 +658,6 @@ function CreatePlantMutationRelation(
} }
function GetPlantRelatedMutations(
PlantId,
RelationName
) {
return Object.values(
MutationSets
)
.filter(
Mutation =>
Mutation.Relations?.[
RelationName
]?.includes(
Number(PlantId)
)
)
.sort(
(A, B) =>
A.Id - B.Id
);
}
function GetPlantRecipeProgress( function GetPlantRecipeProgress(
PlantId PlantId
) { ) {
+1033 -27
View File
File diff suppressed because it is too large. Load diff
+23
View File
@@ -233,6 +233,29 @@ async function SetUsername(
return await Response.json(); return await Response.json();
} }
async function SetUserConfig(
Changes
) {
const Response = await fetch(
ApiUrl + "/SetUserConfig.php",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
SaveKey: GetSaveKey(),
...Changes
})
}
);
return await Response.json();
}
async function SetColour( async function SetColour(
Colour Colour
) { ) {
+90 -5
View File
@@ -1046,7 +1046,7 @@ input, button, textarea, select {
color: var(--Blue); color: var(--Blue);
} }
.ProfileSection input, .ProfileSection textarea, .ProfileSection button { .ProfileSection input, .ProfileSection textarea, .ProfileSection select, .ProfileSection button {
font-size: 1rem; font-size: 1rem;
color: var(--Text); color: var(--Text);
background-color: var(--Surface0); background-color: var(--Surface0);
@@ -1055,7 +1055,7 @@ input, button, textarea, select {
appearance: none; appearance: none;
} }
.ProfileSection input, .ProfileSection textarea { .ProfileSection input, .ProfileSection textarea, .ProfileSection select {
padding: 0.6rem; padding: 0.6rem;
outline: none; outline: none;
} }
@@ -1064,6 +1064,10 @@ input, button, textarea, select {
min-width: 12rem; min-width: 12rem;
} }
.ProfileSection select {
appearance: auto;
}
.ProfileSection textarea { .ProfileSection textarea {
width: 100%; width: 100%;
max-width: 24rem; max-width: 24rem;
@@ -1074,12 +1078,12 @@ input, button, textarea, select {
color: var(--Overlay1); color: var(--Overlay1);
} }
.ProfileSection input:hover, .ProfileSection textarea:hover { .ProfileSection input:hover, .ProfileSection textarea:hover, .ProfileSection select:hover {
background-color: var(--Surface1); background-color: var(--Surface1);
border-color: var(--Overlay2); border-color: var(--Overlay2);
} }
.ProfileSection input:focus, .ProfileSection textarea:focus { .ProfileSection input:focus, .ProfileSection textarea:focus, .ProfileSection select:focus {
background-color: var(--Surface1); background-color: var(--Surface1);
border-color: var(--Sky); border-color: var(--Sky);
} }
@@ -1260,7 +1264,7 @@ input, button, textarea, select {
align-items: stretch; align-items: stretch;
} }
.ProfileSection input, .ProfileSection textarea, .ProfileSection button { .ProfileSection input, .ProfileSection textarea, .ProfileSection select, .ProfileSection button {
width: 100%; width: 100%;
} }
@@ -1524,6 +1528,87 @@ input, button, textarea, select {
color: var(--Text); color: var(--Text);
} }
.ProfilePictureSelectionRow {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(10rem, auto) auto;
gap: 0.75rem;
margin-bottom: 1rem;
align-items: end;
}
.ProfilePictureSelectionLabel {
display: block;
margin-bottom: 0.35rem;
color: var(--Subtext1);
}
.ProfilePictureGrowthStageLabel {
margin-bottom: 0 !important;
}
.ProfilePictureGrowthStageLabel select {
display: block;
width: 100%;
min-width: 10rem;
margin-top: 0.35rem;
cursor: pointer;
}
.ProfilePictureGrid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0.75rem;
max-height: 31rem;
padding: 0.25rem;
overflow-y: auto;
}
.ProfileSection .ProfilePictureOption {
display: flex;
min-width: 0;
padding: 0.5rem;
flex-direction: column;
gap: 0.4rem;
align-items: stretch;
}
.ProfileSection .ProfilePictureOption[aria-pressed="true"] {
color: var(--Yellow);
border-color: var(--Yellow);
}
.ProfilePictureOptionName {
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.ProfilePictureOptionVisual {
display: flex;
width: 100%;
aspect-ratio: 1;
align-items: center;
justify-content: center;
}
.ProfilePictureOptionMissing {
padding: 0.35rem;
color: var(--Overlay1);
font-size: 0.75rem;
text-align: center;
}
@media (max-width: 700px) {
.ProfilePictureSelectionRow {
grid-template-columns: 1fr 1fr;
}
.ProfilePictureSelectionRow > :first-child {
grid-column: 1 / -1;
}
}
/* * Garden guide */ /* * Garden guide */
.GuideSection { .GuideSection {