Skip to content

Commit

Permalink
Merge pull request #19 from SkyCryptWebsite:feat/dungeons
Browse files Browse the repository at this point in the history
Add Dungeons section to stats page
  • Loading branch information
DarthGigi authored Jul 20, 2024
2 parents d01bf25 + 3c0d5e2 commit b894f6a
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 36 deletions.
9 changes: 8 additions & 1 deletion src/lib/components/AdditionStat.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
export let text: string;
export let data: string;
export let subData: string | undefined = undefined;
export let asterisk: boolean = false;
let className: string | null | undefined = undefined;
Expand All @@ -13,7 +14,13 @@
<Tooltip.Root group="additional-stats" openDelay={0} closeDelay={0}>
<Tooltip.Trigger class={cn(`my-0 flex items-center gap-1 text-sm font-bold text-text/60 data-[is-tooltip=false]:cursor-default`, className)} data-is-tooltip={asterisk}>
<div class="capitalize">{text}:</div>
<span class="-mr-0.5 text-text">{data}</span>
<span class="-mr-0.5 text-text"
>{data}
{#if subData}
<span class="text-text/80"> {subData}</span>
{/if}
</span>

{#if asterisk}
*
{/if}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/layouts/stats/Main.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import Stats from "$lib/layouts/stats/Stats.svelte";
import Accessories from "$lib/sections/stats/Accessories.svelte";
import Armor from "$lib/sections/stats/Armor.svelte";
import Dungeons from "$lib/sections/stats/Dungeons.svelte";
import Inventory from "$lib/sections/stats/Inventory.svelte";
import Pets from "$lib/sections/stats/Pets.svelte";
import SkillsSection from "$lib/sections/stats/SkillsSection.svelte";
Expand All @@ -28,6 +29,7 @@
<Pets />
<Inventory />
<SkillsSection />
<Dungeons />
</main>

<svg xmlns="http://www.w3.org/2000/svg" height="0" width="0" style="position: fixed;">
Expand Down
178 changes: 178 additions & 0 deletions src/lib/sections/stats/Dungeons.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<script lang="ts">
import AdditionStat from "$lib/components/AdditionStat.svelte";
import Skillbar from "$lib/components/Skillbar.svelte";
import { formatNumber } from "$lib/tools";
import type { Stats as StatsType } from "$types/stats";
import { Avatar, Collapsible } from "bits-ui";
import { formatDate, formatDistanceToNowStrict } from "date-fns";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import Image from "lucide-svelte/icons/image";
import { format } from "numerable";
import { getContext } from "svelte";
const profile = getContext<StatsType>("profile");
const dungeons = profile.dungeons;
</script>

<div class="space-y-4">
<h3 class="text-2xl uppercase">Dungeons</h3>
{#if dungeons}
<div class="flex flex-col flex-wrap justify-start gap-x-4 gap-y-2 sm:flex-row">
<Skillbar class="" skill="Catacombs" skillData={dungeons.level} />
{#each Object.entries(dungeons.classes.classes) as [className, classData]}
<Skillbar class="sm:last:grow sm:last:basis-1/3" skill={className} skillData={classData} />
{/each}
</div>
<div>
<AdditionStat text="Selected Class" data={dungeons.classes.selectedClass} />
<AdditionStat text="Class Average" data={format(dungeons.classes.classAverage)} asterisk={true}>
<div class="max-w-xs space-y-2 font-bold">
<div>
<h3 class="text-text/85">Total Class XP: {format(dungeons.classes.totalClassExp.toFixed(2))}</h3>
<p class="font-medium italic text-text/80">Total Class XP gained in Catacombs.</p>
</div>
<div>
<h3 class="text-text/85">Average Level: {format(dungeons.classes.classAverageWithProgress.toFixed(2))}</h3>
<p class="font-medium italic text-text/80">Average class level, includes progress to next level.</p>
</div>
<div>
<h3 class="text-text/85">Average Level without progress: {format(dungeons.classes.classAverage.toFixed(2))}</h3>
<p class="font-medium italic text-text/80">Average class level without including partial level progress.</p>
</div>
</div>
</AdditionStat>
<AdditionStat text="Highest Floor Beaten (Normal)" data={format(dungeons.stats.highestFloorBeatenNormal)} />
<AdditionStat text="Highest Floor Beaten (Master)" data={format(dungeons.stats.highestFloorBeatenMaster)} />
<AdditionStat text="Secrets Found" data={format(dungeons.stats.secrets.found)} subData="({format(dungeons.stats.secrets.secretsPerRun.toFixed(2))} S/R)" />
</div>
<div>
<h4 class="mb-5 text-xl font-semibold capitalize">Catacombs</h4>
<div class="flex flex-wrap gap-5">
{#if dungeons.catacombs}
{#each dungeons.catacombs as catacomb}
<div class="flex min-w-80 basis-[calc((100%/3)-1.25rem)] flex-col gap-1 rounded-lg bg-background/30">
<div class="flex w-full items-center justify-center gap-1.5 border-b-2 border-icon py-2 text-center font-semibold uppercase">
<Avatar.Root>
<Avatar.Image src={catacomb.texture} class="size-8 object-contain" />
<Avatar.Fallback>
<Image class="size-8" />
</Avatar.Fallback>
</Avatar.Root>
{catacomb.name}
</div>

<Collapsible.Root class="p-5">
<Collapsible.Trigger class="group flex items-center gap-0.5">
<ChevronDown class="size-4 transition-all duration-300 group-data-[state=open]:-rotate-180" />
<h4 class="text-xl font-semibold capitalize text-text">Floor Stats</h4>
</Collapsible.Trigger>
<Collapsible.Content>
{#each Object.entries(catacomb.stats) as [key, value]}
{#if typeof value === "object"}
<AdditionStat class="capitalize" text={key.toLowerCase().replaceAll("_", " ")} data={formatNumber(value.damage)} subData="({value.type})" />
{:else}
<AdditionStat class="capitalize" text={key.toLowerCase().replaceAll("_", " ")} data={formatNumber(value)} />
{/if}
{/each}
</Collapsible.Content>
</Collapsible.Root>

{#if catacomb.best_run}
<Collapsible.Root class="px-5 pb-[2.5rem]">
<Collapsible.Trigger class="group flex items-center gap-0.5">
<ChevronDown class="size-4 transition-all duration-300 group-data-[state=open]:-rotate-180" />
<h4 class="text-xl font-semibold capitalize text-text">Best run</h4>
</Collapsible.Trigger>
<Collapsible.Content>
{#each Object.entries(catacomb.best_run) as [key, value]}
{#if typeof value === "number"}
{#if key === "timestamp"}
<AdditionStat class="capitalize" text={key.toLowerCase().replaceAll("_", " ")} data={formatDistanceToNowStrict(value, { addSuffix: true })} asterisk={true}>
{formatDate(value, "dd MMMM yyyy 'at' HH:mm")}
</AdditionStat>
{:else}
<AdditionStat class="capitalize" text={key.toLowerCase().replaceAll("_", " ")} data={formatNumber(value)} />
{/if}
{:else}
<AdditionStat class="capitalize" text={key.toLowerCase().replaceAll("_", " ")} data={value} />
{/if}
{/each}
</Collapsible.Content>
</Collapsible.Root>
{:else}
<div class="p-5 text-center">This player has not played this floor.</div>
{/if}
</div>
{/each}
{:else}
This player has not played any Catacombs.
{/if}
</div>
</div>

<div>
<h4 class="mb-5 text-xl font-semibold capitalize">Master Catacombs</h4>
<div class="flex flex-wrap gap-5">
{#if dungeons.master_catacombs}
{#each dungeons.master_catacombs as catacomb}
<div class="flex min-w-80 basis-[calc((100%/3)-1.25rem)] flex-col gap-1 rounded-lg bg-background/30">
<div class="flex w-full items-center justify-center gap-1.5 border-b-2 border-icon py-2 text-center font-semibold uppercase">
<Avatar.Root>
<Avatar.Image src={catacomb.texture} class="size-8 object-contain" />
<Avatar.Fallback>
<Image class="size-8" />
</Avatar.Fallback>
</Avatar.Root>
{catacomb.name}
</div>

<Collapsible.Root class="p-5">
<Collapsible.Trigger class="group flex items-center gap-0.5">
<ChevronDown class="size-4 transition-all duration-300 group-data-[state=open]:-rotate-180" />
<h4 class="text-xl font-semibold capitalize text-text">Floor Stats</h4>
</Collapsible.Trigger>
<Collapsible.Content>
{#each Object.entries(catacomb.stats) as [key, value]}
{#if typeof value === "object"}
<AdditionStat class="capitalize" text={key.toLowerCase().replaceAll("_", " ")} data={formatNumber(value.damage)} subData="({value.type})" />
{:else}
<AdditionStat class="capitalize" text={key.toLowerCase().replaceAll("_", " ")} data={formatNumber(value)} />
{/if}
{/each}
</Collapsible.Content>
</Collapsible.Root>

{#if catacomb.best_run}
<Collapsible.Root class="px-5 pb-[2.5rem]">
<Collapsible.Trigger class="group flex items-center gap-0.5">
<ChevronDown class="size-4 transition-all duration-300 group-data-[state=open]:-rotate-180" />
<h4 class="text-xl font-semibold capitalize text-text">Best run</h4>
</Collapsible.Trigger>
<Collapsible.Content>
{#each Object.entries(catacomb.best_run) as [key, value]}
{#if typeof value === "number"}
{#if key === "timestamp"}
<AdditionStat class="capitalize" text={key.toLowerCase().replaceAll("_", " ")} data={formatDistanceToNowStrict(value, { addSuffix: true })} asterisk={true}>
{formatDate(value, "dd MMMM yyyy 'at' HH:mm")}
</AdditionStat>
{:else}
<AdditionStat class="capitalize" text={key.toLowerCase().replaceAll("_", " ")} data={formatNumber(value)} />
{/if}
{:else}
<AdditionStat class="capitalize" text={key.toLowerCase().replaceAll("_", " ")} data={value} />
{/if}
{/each}
</Collapsible.Content>
</Collapsible.Root>
{:else}
<div class="p-5 text-center">This player has not played this floor.</div>
{/if}
</div>
{/each}
{:else}
This player has not played any Master Catacombs.
{/if}
</div>
</div>
{/if}
</div>
20 changes: 19 additions & 1 deletion src/lib/stats/bestiary.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Member } from "$types/global";
import * as constants from "$constants/constants";
import type { Member } from "$types/global";
import type { BestiaryStats } from "$types/processed/profile/bestiary";

function getBestiaryMobs(
Expand Down Expand Up @@ -35,6 +35,24 @@ function getBestiaryMobs(
return output;
}

export function getBestiaryFamily(userProfile: Member, mobName: string) {
const bestiary = userProfile.bestiary.kills || {};
const family = Object.values(constants.BESTIARY)
.flatMap((category) => category.mobs)
.find((mob) => mob.name === mobName);

if (family === undefined) {
return null;
}

const output = getBestiaryMobs(bestiary, [family]);
if (!output.length) {
return null;
}

return output[0];
}

export function getBestiary(userProfile: Member) {
const bestiary = userProfile.bestiary.kills || {};

Expand Down
54 changes: 36 additions & 18 deletions src/lib/stats/dungeons.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as constants from "$constants/constants";
import * as helper from "$lib/helper";
import type { BestRun, Catacombs, Member, Skill } from "$types/global";
import { getBestiaryFamily } from "./bestiary";
import { getLevelByXp } from "./leveling/leveling";

function getDungeonClasses(userProfile: Member) {
Expand Down Expand Up @@ -85,6 +86,22 @@ function getBestRun(catacombs: Catacombs, floorId: number) {
};
}

function getSecrets(catacombs: Member["dungeons"]) {
const secretsFound = catacombs.secrets ?? 0;
const totalRuns =
Object.keys(catacombs?.dungeon_types?.catacombs?.tier_completions || {})
.filter((key) => key !== "total")
.reduce((a, b) => a + (catacombs?.dungeon_types?.catacombs?.tier_completions[b] || 0), 0) +
Object.keys(catacombs?.dungeon_types?.master_catacombs?.tier_completions || {})
.filter((key) => key !== "total")
.reduce((a, b) => a + (catacombs?.dungeon_types?.master_catacombs?.tier_completions[b] || 0), 0);

return {
found: secretsFound,
secretsPerRun: secretsFound / totalRuns
};
}

function formatCatacombsData(catacombs: Catacombs) {
const type = catacombs.experience ? "catacombs" : "master_catacombs";

Expand All @@ -94,22 +111,20 @@ function formatCatacombsData(catacombs: Catacombs) {
output.push({
name: floor.name,
texture: floor.texture,

times_played: catacombs.times_played?.[floor.id] ?? 0,
tier_completions: catacombs.tier_completions?.[floor.id] ?? 0,
milestone_completions: catacombs.milestone_completions?.[floor.id] ?? 0,
best_score: catacombs.best_score?.[floor.id] ?? 0,

mobs_killed: catacombs.mobs_killed?.[floor.id] ?? 0,
watcher_kills: catacombs.watcher_kills?.[floor.id] ?? 0,
most_mobs_killed: catacombs.most_mobs_killed?.[floor.id] ?? 0,

fastest_time: catacombs.fastest_time?.[floor.id] ?? 0,
fastest_time_s: catacombs.fastest_time?.[floor.id] ?? 0,
fastest_time_s_plus: catacombs.fastest_time_s_plus?.[floor.id] ?? 0,

most_healing: catacombs.most_healing?.[floor.id] ?? 0,
most_damage: getMostDamage(catacombs, floor.id),
stats: {
times_played: catacombs.times_played?.[floor.id] ?? 0,
tier_completions: catacombs.tier_completions?.[floor.id] ?? 0,
milestone_completions: catacombs.milestone_completions?.[floor.id] ?? 0,
best_score: catacombs.best_score?.[floor.id] ?? 0,
mobs_killed: catacombs.mobs_killed?.[floor.id] ?? 0,
watcher_kills: catacombs.watcher_kills?.[floor.id] ?? 0,
most_mobs_killed: catacombs.most_mobs_killed?.[floor.id] ?? 0,
fastest_time: catacombs.fastest_time?.[floor.id] ?? 0,
fastest_time_s: catacombs.fastest_time?.[floor.id] ?? 0,
fastest_time_s_plus: catacombs.fastest_time_s_plus?.[floor.id] ?? 0,
most_healing: catacombs.most_healing?.[floor.id] ?? 0,
most_damage: getMostDamage(catacombs, floor.id)
},
best_run: getBestRun(catacombs, floor.id)
});
}
Expand All @@ -133,8 +148,11 @@ export function getDungeons(userProfile: Member) {
classAverageWithProgress: Object.values(dungeonClasses).reduce((a, b) => a + b.levelWithProgress, 0) / Object.keys(dungeonClasses).length,
totalClassExp: Object.values(userProfile.dungeons.player_classes).reduce((a, b) => a + b.experience, 0)
},
secrets: {
found: userProfile.dungeons.secrets
stats: {
secrets: getSecrets(userProfile.dungeons),
highestFloorBeatenNormal: userProfile.dungeons.dungeon_types.catacombs.highest_tier_completed ?? 0,
highestFloorBeatenMaster: userProfile.dungeons.dungeon_types.master_catacombs.highest_tier_completed ?? 0,
bloodMobKills: getBestiaryFamily(userProfile, "Undead")?.kills ?? 0
},
catacombs: formatCatacombsData(userProfile.dungeons.dungeon_types.catacombs),
master_catacombs: formatCatacombsData(userProfile.dungeons.dungeon_types.master_catacombs)
Expand Down
40 changes: 24 additions & 16 deletions src/types/processed/profile/dungeons.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ export type DungeonsStats = {
classAverageWithProgress: number;
totalClassExp: number;
};
secrets: {
found: number;
stats: {
secrets: {
found: number;
secretsPerRun: number;
};
highestFloorBeatenNormal: number;
highestFloorBeatenMaster: number;
bloodMobKills: number;
};
catacombs: CatacombsData[];
master_catacombs: CatacombsData[];
Expand All @@ -19,20 +25,22 @@ export type DungeonsStats = {
export type CatacombsData = {
name: string;
texture: string;
times_played: number;
tier_completions: number;
milestone_completions: number;
best_score: number;
mobs_killed: number;
watcher_kills: number;
most_mobs_killed: number;
fastest_time: number;
fastest_time_s: number;
fastest_time_s_plus: number;
most_healing: number;
most_damage: {
damage: number;
type: string;
stats: {
times_played: number;
tier_completions: number;
milestone_completions: number;
best_score: number;
mobs_killed: number;
watcher_kills: number;
most_mobs_killed: number;
fastest_time: number;
fastest_time_s: number;
fastest_time_s_plus: number;
most_healing: number;
most_damage: {
damage: number;
type: string;
};
};
best_run: {
grade: string;
Expand Down

0 comments on commit b894f6a

Please sign in to comment.