diff options
author | mat <github@matdoes.dev> | 2022-04-23 22:08:29 -0500 |
---|---|---|
committer | mat <github@matdoes.dev> | 2022-04-23 22:08:29 -0500 |
commit | fa42cdc56e21d6d50f17db6143b74d8b57f179be (patch) | |
tree | 0ea27306653c23b683d89ffe8344e72f0626cd2a | |
parent | 0f0a7eb82120a7cdaa392bc33983be656b85750f (diff) | |
download | skyblock-stats-fa42cdc56e21d6d50f17db6143b74d8b57f179be.tar.gz skyblock-stats-fa42cdc56e21d6d50f17db6143b74d8b57f179be.tar.bz2 skyblock-stats-fa42cdc56e21d6d50f17db6143b74d8b57f179be.zip |
add /items page
-rw-r--r-- | src/lib/APITypes.d.ts | 18 | ||||
-rw-r--r-- | src/lib/api.ts | 3 | ||||
-rw-r--r-- | src/lib/minecraft/Item.svelte | 35 | ||||
-rw-r--r-- | src/lib/minecraft/MinecraftTooltip.svelte | 2 | ||||
-rw-r--r-- | src/lib/minecraft/inventory.ts | 21 | ||||
-rw-r--r-- | src/lib/utils.ts | 14 | ||||
-rw-r--r-- | src/routes/items.svelte | 213 |
7 files changed, 296 insertions, 10 deletions
diff --git a/src/lib/APITypes.d.ts b/src/lib/APITypes.d.ts index 7a6039c..827ee13 100644 --- a/src/lib/APITypes.d.ts +++ b/src/lib/APITypes.d.ts @@ -299,13 +299,23 @@ export interface PetsData { } export interface ItemRequirement { - dungeon: { + dungeon?: { type: string level: number } + skill?: { + type: string + level: number + } + slayer?: { + boss: string + level: number + } } + export interface ItemListItem { id: string + headTexture?: string vanillaId: string tier: string | null display: { @@ -313,8 +323,12 @@ export interface ItemListItem { glint: boolean } npcSellPrice: number | null - requirements: ItemRequirement | null + requirements: ItemRequirement + category: string | null + soulbound: boolean + museum: boolean } + export interface ItemListData { lastUpdated: number list: ItemListItem[] diff --git a/src/lib/api.ts b/src/lib/api.ts index 243cf2b..4ffa87d 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1 +1,2 @@ -export const API_URL = 'https://skyblock-api.matdoes.dev/' +// export const API_URL = 'https://skyblock-api.matdoes.dev/' +export const API_URL = 'http://localhost:8080/' diff --git a/src/lib/minecraft/Item.svelte b/src/lib/minecraft/Item.svelte index 4d0997c..cfcc219 100644 --- a/src/lib/minecraft/Item.svelte +++ b/src/lib/minecraft/Item.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { formattingCodeToHtml, removeFormattingCode } from '$lib/utils' + import { cleanId, formattingCodeToHtml, removeFormattingCode, TIER_COLORS } from '$lib/utils' import MinecraftTooltip from './MinecraftTooltip.svelte' import type { MatcherFile } from 'skyblock-assets' import { itemToUrl } from './inventory' @@ -13,8 +13,35 @@ let itemNameHtml: string | null let imageUrl: string | null - $: itemLoreHtml = item ? item.display.lore.map(l => formattingCodeToHtml(l)).join('<br>') : null - $: itemNameHtml = item ? formattingCodeToHtml(item.display.name) : null + let extraLore: string[] = [] + if (!item?.display?.lore && item?.tier) { + // ☠ &cRequires &5Enderman Slayer 7 + if (item.requirements.slayer) + extraLore.push( + `§4☠ §cRequires §5${cleanId(item.requirements.slayer.boss)} Slayer ${ + item.requirements.slayer.level + }` + ) + extraLore.push(`§l§${TIER_COLORS[item.tier] ?? 'c'}${item.tier}`) + } + if (item?.id) { + extraLore.push(`\n§8ID: ${item.id}`) + } + + $: itemLoreHtml = item + ? (item.display?.lore ?? []) + .concat(extraLore) + .map(l => formattingCodeToHtml(l)) + .join('<br>') + : null + $: { + let itemDisplayName = item?.display?.name + if (itemDisplayName) { + if (!itemDisplayName.includes('§') && item.tier) + itemDisplayName = `§${TIER_COLORS[item.tier] ?? 'c'}${itemDisplayName}` + } + itemNameHtml = itemDisplayName ? formattingCodeToHtml(itemDisplayName) : null + } $: imageUrl = item ? itemToUrl(item, pack, headSize) : null </script> @@ -33,7 +60,7 @@ class:item-custom-head={imageUrl.startsWith('https://mc-heads.net/head/')} /> {/if} - {#if item.count !== 1} + {#if item.count !== undefined && item.count !== 1} <span class="item-count">{item.count}</span> {/if} </span> diff --git a/src/lib/minecraft/MinecraftTooltip.svelte b/src/lib/minecraft/MinecraftTooltip.svelte index 0cdb237..7149626 100644 --- a/src/lib/minecraft/MinecraftTooltip.svelte +++ b/src/lib/minecraft/MinecraftTooltip.svelte @@ -26,6 +26,8 @@ .minecraft-tooltip { /* this makes it be less dumb about the height so it doesn't add extra or anything */ display: grid; + /* this is so it doesn't change width to fill the container in the item list page */ + max-width: fit-content; } /* these elements exist so we can copy them later from GlobalTooltip */ .tooltip-name, diff --git a/src/lib/minecraft/inventory.ts b/src/lib/minecraft/inventory.ts index 871af93..ec0e193 100644 --- a/src/lib/minecraft/inventory.ts +++ b/src/lib/minecraft/inventory.ts @@ -1,6 +1,6 @@ import * as skyblockAssets from 'skyblock-assets' import { vanilla } from '$lib/packs' - +import { browser } from '$app/env' export interface Item { id?: string @@ -97,6 +97,13 @@ export const inventoryIconMap: Record<string, string | Item> = { export type Inventories = { [name in keyof typeof INVENTORIES]: Item[] } +// we cache the item urls because it takes a bit of time to get them usually +// { "<pack> <item id>": "https://..." } +let itemUrlCache: Record<string, string> = {} +// clear the cache every 120 seconds, this number is arbitrary +setInterval(() => { + itemUrlCache = {} +}, 120 * 1000) export function itemToUrl(item: Item, pack?: skyblockAssets.MatcherFile, headSize?: number): string { const itemNbt: skyblockAssets.NBT = { display: { @@ -107,6 +114,11 @@ export function itemToUrl(item: Item, pack?: skyblockAssets.MatcherFile, headSiz }, } let textureUrl: string + + const itemCacheIdentifier = `${pack?.dir ?? 'v'} ${JSON.stringify(itemNbt)}` + if (itemCacheIdentifier in itemUrlCache) + return itemUrlCache[itemCacheIdentifier] + if (item.headTexture) { // if it's a head, try without vanilla and if it fails just use the mc-heads url textureUrl = skyblockAssets.getTextureUrl({ @@ -120,21 +132,24 @@ export function itemToUrl(item: Item, pack?: skyblockAssets.MatcherFile, headSiz if (headSize) textureUrl += `/${headSize}` } - } else + } else { textureUrl = skyblockAssets.getTextureUrl({ id: item.vanillaId, nbt: itemNbt, packs: pack ? [pack, vanilla as skyblockAssets.MatcherFile] : [vanilla as skyblockAssets.MatcherFile], }) + } + itemUrlCache[itemCacheIdentifier] = textureUrl return textureUrl } export function skyblockItemToUrl(skyblockItem: string | Item, pack?: skyblockAssets.MatcherFile, headSize?: number) { - const item: Item = typeof skyblockItem === 'string' ? skyblockItemNameToItem(skyblockItem) : skyblockItem + const item = typeof skyblockItem === 'string' ? skyblockItemNameToItem(skyblockItem) : skyblockItem const itemTextureUrl = itemToUrl(item, pack, headSize) return itemTextureUrl } + export function skyblockItemNameToItem(skyblockItemName: string): Item { let item: Item if (Object.keys(skyblockItems).includes(skyblockItemName)) { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e7bebb2..e34d573 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -17,6 +17,18 @@ export const colorCodes: { [key: string]: string } = { 'f': '#ffffff', // white } +export const TIER_COLORS = { + COMMON: 'f', + UNCOMMON: 'a', + RARE: '9', + EPIC: '5', + LEGENDARY: '6', + MYTHIC: 'e', + DIVINE: 'b', + SPECIAL: 'c', + VERY_SPECIAL: 'c', +} + const specialCodes: { [key: string]: string } = { 'l': 'font-weight: bold' } @@ -60,6 +72,8 @@ export function formattingCodeToHtml(formatted: string): string { } else if (colorCharacter === 'r') { reset() } + } else if (character === '\n') { + htmlOutput += '<br>' } else { // no xss! htmlOutput += character.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') diff --git a/src/routes/items.svelte b/src/routes/items.svelte new file mode 100644 index 0000000..a14dd85 --- /dev/null +++ b/src/routes/items.svelte @@ -0,0 +1,213 @@ +<script lang="ts" context="module"> + import type { Load } from '@sveltejs/kit' + import { API_URL } from '$lib/api' + + export const load: Load = async ({ params, fetch }) => { + const data = await fetch(`${API_URL}items`).then(r => r.json()) + + return { + props: { + data, + }, + } + } +</script> + +<script lang="ts"> + import Header from '$lib/Header.svelte' + import Head from '$lib/Head.svelte' + import { millisecondsToTime, TIER_COLORS, colorCodes, cleanId, toTitleCase } from '$lib/utils' + import type { ItemListData, ItemListItem } from '$lib/APITypes' + import Item from '$lib/minecraft/Item.svelte' + import furfskyReborn from 'skyblock-assets/matchers/furfsky_reborn.json' + import { onMount } from 'svelte' + + export let data: ItemListData + + let now = Date.now() + + let query: string = '' + let filterCategory = 'any' + let filterTier = 'any' + // this is pre-calculated here so we don't have to keep lowercasing it + $: normalizedQuery = query.toLowerCase().replace(/_/g, ' ') + + $: filteredItems = data.list + .filter( + i => + i.display.name.toLowerCase().replace(/_/g, ' ').includes(normalizedQuery) || + i.id.toLowerCase().replace(/_/g, ' ').includes(normalizedQuery) + ) + .filter(i => filterCategory === 'any' || i.category === filterCategory) + .filter(i => filterTier === 'any' || i.tier === filterTier) + + let pageHeight = 0 + let filteredItemsSliced: ItemListItem[] + $: { + filteredItemsSliced = filteredItems.slice(0, 200) + pageHeight = 0 + } + + function checkScroll() { + let pageHeightTemp = window.scrollY + window.innerHeight + if (pageHeightTemp <= pageHeight) return + pageHeight = pageHeightTemp + if (pageHeight >= document.body.scrollHeight - 1000) { + filteredItemsSliced = filteredItems.slice(0, filteredItemsSliced.length + 200) + } + } + + let categories: Set<string> = new Set() + let tiers: Set<string> = new Set() + for (const i of data.list) { + if (i.category) categories.add(i.category) + if (i.tier) tiers.add(i.tier) + } +</script> + +<Head title="SkyBlock Item List" /> +<Header /> + +<svelte:window on:scroll={checkScroll} /> + +<main> + <div class="last-updated"> + Last updated: <time datetime="${data.lastUpdated}" + >{millisecondsToTime(now - data.lastUpdated)} ago + </time> + </div> + <h1>SkyBlock Item List</h1> + <p class="results-count">{filteredItems.length.toLocaleString()} items.</p> + <div class="filter-items-settings"> + <input type="text" id="filter-items-tier" placeholder="Search..." bind:value={query} /> + + <label for="filter-items-tier">Tier:</label> + <select id="filter-items-tier" class="filter-items-tier" bind:value={filterTier}> + <option value="any" selected>Any</option> + {#each Array.from(tiers) as tier} + <option value={tier}>{cleanId(tier.toLowerCase())}</option> + {/each} + </select> + + <label for="filter-items-category">Category:</label> + <select id="filter-items-category" class="filter-items-category" bind:value={filterCategory}> + <option value="any" selected>Any</option> + {#each Array.from(categories) as category} + <option value={category}>{cleanId(category)}</option> + {/each} + </select> + </div> + <div class="item-list"> + {#each filteredItemsSliced as item (item.id)} + <div class="item-container"> + <span class="item-slot-container"><Item {item} pack={furfskyReborn} /></span> + <h3 + class="item-name" + style={item.tier ? `color: ${colorCodes[TIER_COLORS[item.tier]]}` : undefined} + > + {item.display.name} + </h3> + <div> + {#if item.museum} + <p> + <span class="item-info-label">Museum</span> + </p> + {/if} + {#if item.soulbound} + <p> + <span class="item-info-label">Soulbound</span> + </p> + {/if} + {#if item.category} + <p> + <span class="item-info-label">Category:</span> + <span class="item-info-value"> + {toTitleCase(cleanId(item.category))} + </span> + </p> + {/if} + {#if item.npcSellPrice} + <p> + <span class="item-info-label">NPC sell price:</span> + <span class="item-info-value"> + {#if item.npcSellPrice == 1}1 coin{:else} + {item.npcSellPrice.toLocaleString()} coins{/if} + </span> + </p> + {/if} + {#if Object.keys(item.requirements).length > 0} + <div> + <span class="item-info-label">Requirements:</span> + <ul> + {#if item.requirements.dungeon} + <li> + {cleanId(item.requirements.dungeon.type)} + {item.requirements.dungeon.level} + </li> + {/if} + {#if item.requirements.skill} + <li> + {cleanId(item.requirements.skill.type)} + {item.requirements.skill.level} + </li> + {/if} + {#if item.requirements.slayer} + <li> + {cleanId(item.requirements.slayer.boss)} + Slayer + {item.requirements.slayer.level} + </li> + {/if} + </ul> + </div> + {/if} + </div> + </div> + {/each} + {#if filteredItems.length === 0} + No results! + {/if} + </div> +</main> + +<style> + .last-updated { + float: right; + color: var(--theme-darker-text); + } + + p, + ul { + margin: 0; + } + li { + position: relative; + left: -1rem; + } + .results-count { + position: relative; + top: -1em; + } + .item-slot-container { + float: right; + } + .item-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); + grid-gap: 1em; + margin-top: 1em; + } + .item-container { + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.1); + padding: 0.75em; + border-radius: 1em; + } + + .item-info-label { + color: var(--theme-darker-text); + } + .item-info-value { + color: var(--theme-main-text); + } +</style> |