aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <github@matdoes.dev>2022-04-23 22:08:29 -0500
committermat <github@matdoes.dev>2022-04-23 22:08:29 -0500
commitfa42cdc56e21d6d50f17db6143b74d8b57f179be (patch)
tree0ea27306653c23b683d89ffe8344e72f0626cd2a
parent0f0a7eb82120a7cdaa392bc33983be656b85750f (diff)
downloadskyblock-stats-fa42cdc56e21d6d50f17db6143b74d8b57f179be.tar.gz
skyblock-stats-fa42cdc56e21d6d50f17db6143b74d8b57f179be.tar.bz2
skyblock-stats-fa42cdc56e21d6d50f17db6143b74d8b57f179be.zip
add /items page
-rw-r--r--src/lib/APITypes.d.ts18
-rw-r--r--src/lib/api.ts3
-rw-r--r--src/lib/minecraft/Item.svelte35
-rw-r--r--src/lib/minecraft/MinecraftTooltip.svelte2
-rw-r--r--src/lib/minecraft/inventory.ts21
-rw-r--r--src/lib/utils.ts14
-rw-r--r--src/routes/items.svelte213
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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>